From d72057f41b4306b1e65efa567d04a7e3e482991d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 7 Aug 2023 10:52:14 +0100 Subject: [PATCH 001/179] Add repair issue for Reolink when using it with an incompatible global ssl certificate (#91597) --- homeassistant/components/reolink/host.py | 20 ++++++++++++++ homeassistant/components/reolink/strings.json | 4 +++ tests/components/reolink/test_init.py | 27 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 9bcafb8f00d..feeff9312c7 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -231,6 +231,7 @@ class ReolinkHost: "network_link": "https://my.home-assistant.io/redirect/network/", }, ) + if self._base_url.startswith("https"): ir.async_create_issue( self._hass, @@ -246,9 +247,28 @@ class ReolinkHost: ) else: ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") + + if self._hass.config.api is not None and self._hass.config.api.use_ssl: + ir.async_create_issue( + self._hass, + DOMAIN, + "ssl", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="ssl", + translation_placeholders={ + "ssl_link": "https://www.home-assistant.io/integrations/http/#ssl_certificate", + "base_url": self._base_url, + "network_link": "https://my.home-assistant.io/redirect/network/", + "nginx_link": "https://github.com/home-assistant/addons/tree/master/nginx_proxy", + }, + ) + else: + ir.async_delete_issue(self._hass, DOMAIN, "ssl") else: ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") + ir.async_delete_issue(self._hass, DOMAIN, "ssl") # If no ONVIF push or long polling state is received, start fast polling await self._async_poll_all_motion() diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 2389c433b20..806f2498094 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -44,6 +44,10 @@ "title": "Reolink webhook URL uses HTTPS (SSL)", "description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device" }, + "ssl": { + "title": "Reolink incompatible with global SSL certificate", + "description": "Global SSL certificate configured in the [configuration.yaml under http]({ssl_link}) while a local HTTP address `{base_url}` is configured under \"Home Assistant URL\" in the [network settings]({network_link}). Therefore the Reolink device can not reach Home Assistant to push its motion/AI events. Please make sure the local HTTP adress is not covered by the SSL certificate, by for instance using [NGINX add-on]({nginx_link}) instead of a globally enforced SSL certificate." + }, "webhook_url": { "title": "Reolink webhook URL unreachable", "description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received." diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index f5f581760c1..8558ff0e8a2 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -10,6 +10,7 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -106,6 +107,7 @@ async def test_no_repair_issue( assert (const.DOMAIN, "webhook_url") not in issue_registry.issues assert (const.DOMAIN, "enable_port") not in issue_registry.issues assert (const.DOMAIN, "firmware_update") not in issue_registry.issues + assert (const.DOMAIN, "ssl") not in issue_registry.issues async def test_https_repair_issue( @@ -130,6 +132,31 @@ async def test_https_repair_issue( assert (const.DOMAIN, "https_webhook") in issue_registry.issues +async def test_ssl_repair_issue( + hass: HomeAssistant, config_entry: MockConfigEntry, reolink_ONVIF_wait: MagicMock +) -> None: + """Test repairs issue is raised when global ssl certificate is used.""" + assert await async_setup_component(hass, "webhook", {}) + hass.config.api.use_ssl = True + + await async_process_ha_core_config( + hass, {"country": "GB", "internal_url": "http://test_homeassistant_address"} + ) + + with patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0 + ), patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 + ), patch( + "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + issue_registry = ir.async_get(hass) + assert (const.DOMAIN, "ssl") in issue_registry.issues + + @pytest.mark.parametrize("protocol", ["rtsp", "rtmp"]) async def test_port_repair_issue( hass: HomeAssistant, From 5232b6ee6a137c23ef43cef5d0fda91ab522a6ae Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 7 Aug 2023 12:08:19 +0200 Subject: [PATCH 002/179] Bump devolo_plc_api to 1.4.0 (#97951) --- homeassistant/components/devolo_home_network/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_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 54b65c17e60..a047437e980 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["devolo_plc_api"], "quality_scale": "platinum", - "requirements": ["devolo-plc-api==1.3.2"], + "requirements": ["devolo-plc-api==1.4.0"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 696a931634f..85770240f74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -661,7 +661,7 @@ denonavr==0.11.3 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.3.2 +devolo-plc-api==1.4.0 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fb487aea9e..877aa2f6a95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -538,7 +538,7 @@ denonavr==0.11.3 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.3.2 +devolo-plc-api==1.4.0 # homeassistant.components.directv directv==0.4.0 From b317d36d0f9ca3830c6bf5c0bfd2a509fd981c7a Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 7 Aug 2023 12:32:59 +0200 Subject: [PATCH 003/179] Bump pyoverkiz to 1.10.1 (#97916) Co-authored-by: J. Nick Koston --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index d88996c7e02..8cf029adb54 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.9.0"], + "requirements": ["pyoverkiz==1.10.1"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 85770240f74..df1ab0595ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.9.0 +pyoverkiz==1.10.1 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 877aa2f6a95..891929c5391 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1417,7 +1417,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.9.0 +pyoverkiz==1.10.1 # homeassistant.components.openweathermap pyowm==3.2.0 From cf4287cd0c2fc6cd3ea49abedb07d3fcc6b53f1c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:57:37 +0200 Subject: [PATCH 004/179] Fix alexa test RuntimeWarning (#97956) --- tests/components/alexa/test_smart_home.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 317febcfdd1..d24ece9b48c 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,6 +1,6 @@ """Test for smart home alexa support.""" from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -4393,6 +4393,7 @@ async def test_alexa_config( assert test_config.should_expose("sensor.test") assert not test_config.should_expose("switch.test") with patch.object(test_config, "_auth", AsyncMock()): + test_config._auth.async_invalidate_access_token = MagicMock() test_config.async_invalidate_access_token() assert len(test_config._auth.async_invalidate_access_token.mock_calls) await test_config.async_accept_grant("grant_code") From 301eaa30b139d773b7580d92ba5dbe20a1433d48 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Aug 2023 13:14:33 +0200 Subject: [PATCH 005/179] Neato add yaml config removal issue (#97447) --- homeassistant/components/neato/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index e7b402aed36..4daa7e5b14d 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -12,9 +12,10 @@ from homeassistant.components.application_credentials import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from . import api @@ -65,6 +66,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "automatically and can be safely removed from your " "configuration.yaml file" ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{NEATO_DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=NEATO_DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": NEATO_DOMAIN, + "integration_title": "Neato Botvac", + }, + ) return True From eff7b8f81aa49a7e8b102ac3c982870a731f78a8 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 7 Aug 2023 07:27:51 -0400 Subject: [PATCH 006/179] Update enphase_envoy codeowners (#97947) --- CODEOWNERS | 4 ++-- homeassistant/components/enphase_envoy/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 012f256f372..e8617ad7703 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -343,8 +343,8 @@ build.json @home-assistant/supervisor /homeassistant/components/enigma2/ @fbradyirl /homeassistant/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer -/homeassistant/components/enphase_envoy/ @gtdiehl -/tests/components/enphase_envoy/ @gtdiehl +/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek +/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek /homeassistant/components/entur_public_transport/ @hfurubotten /homeassistant/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 831553bd312..b6ccbf7e548 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -1,7 +1,7 @@ { "domain": "enphase_envoy", "name": "Enphase Envoy", - "codeowners": ["@gtdiehl"], + "codeowners": ["@bdraco", "@cgarwood", "@dgomes", "@joostlek"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", From 0a2ff3a676baecd069d6a2c86ef870b29d0cbcc8 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 7 Aug 2023 05:01:35 -0700 Subject: [PATCH 007/179] Android TV Remote: Fix missing key and cert when adding a device via IP address (#97953) Fix missing key and cert --- homeassistant/components/androidtv_remote/config_flow.py | 1 + tests/components/androidtv_remote/test_config_flow.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py index b8399fd7ba2..d5c361674bd 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -58,6 +58,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self.host api = create_api(self.hass, self.host, enable_ime=False) try: + await api.async_generate_cert_if_missing() self.name, self.mac = await api.async_get_name_and_mac() assert self.mac await self.async_set_unique_id(format_mac(self.mac)) diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index 4e0067152e7..a2792efb0f3 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -90,6 +90,7 @@ async def test_user_flow_cannot_connect( host = "1.2.3.4" + mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True) mock_api.async_get_name_and_mac = AsyncMock(side_effect=CannotConnect()) result = await hass.config_entries.flow.async_configure( @@ -101,6 +102,7 @@ async def test_user_flow_cannot_connect( assert "host" in result["data_schema"].schema assert result["errors"] == {"base": "cannot_connect"} + mock_api.async_generate_cert_if_missing.assert_called() mock_api.async_get_name_and_mac.assert_called() mock_api.async_start_pairing.assert_not_called() @@ -329,6 +331,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( assert "host" in result["data_schema"].schema assert not result["errors"] + mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True) mock_api.async_get_name_and_mac = AsyncMock(return_value=(name, mac)) result = await hass.config_entries.flow.async_configure( @@ -338,6 +341,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" + mock_api.async_generate_cert_if_missing.assert_called() mock_api.async_get_name_and_mac.assert_called() mock_api.async_start_pairing.assert_not_called() @@ -386,6 +390,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry( assert "host" in result["data_schema"].schema assert not result["errors"] + mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True) mock_api.async_get_name_and_mac = AsyncMock(return_value=(name, mac)) result = await hass.config_entries.flow.async_configure( @@ -395,6 +400,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry( assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" + mock_api.async_generate_cert_if_missing.assert_called() mock_api.async_get_name_and_mac.assert_called() mock_api.async_start_pairing.assert_not_called() From 683c2f8d22c7ea89f8d9cbc1fe2fb38d97c87871 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Aug 2023 14:05:37 +0200 Subject: [PATCH 008/179] Add service for getting a weather forecast (#97078) * Add service for getting a weather forecast * Fix translations * Improve service description * Improve error handling * Adjust typing * Adjust typing * Adjust service response format --- homeassistant/components/weather/__init__.py | 69 +++++++++- .../components/weather/services.yaml | 18 +++ homeassistant/components/weather/strings.json | 21 +++ .../components/weather/websocket_api.py | 3 +- homeassistant/helpers/selector.py | 2 + tests/components/weather/test_init.py | 122 ++++++++++++++++++ 6 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/weather/services.yaml diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index bfad18cb84a..7bd897bb638 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -9,6 +9,8 @@ import inspect import logging from typing import Any, Final, Literal, Required, TypedDict, final +import voluptuous as vol + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PRECISION_HALVES, @@ -18,7 +20,15 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -26,6 +36,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType +from homeassistant.util.json import JsonValueType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .const import ( # noqa: F401 @@ -103,6 +114,8 @@ SCAN_INTERVAL = timedelta(seconds=30) ROUNDING_PRECISION = 2 +SERVICE_GET_FORECAST: Final = "get_forecast" + # mypy: disallow-any-generics @@ -158,6 +171,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = hass.data[DOMAIN] = EntityComponent[WeatherEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) + component.async_register_entity_service( + SERVICE_GET_FORECAST, + {vol.Required("type"): vol.In(("daily", "hourly", "twice_daily"))}, + async_get_forecast_service, + required_features=[ + WeatherEntityFeature.FORECAST_DAILY, + WeatherEntityFeature.FORECAST_HOURLY, + WeatherEntityFeature.FORECAST_TWICE_DAILY, + ], + supports_response=SupportsResponse.ONLY, + ) async_setup_ws_api(hass) await component.async_setup(config) return True @@ -238,7 +262,7 @@ class WeatherEntity(Entity): _forecast_listeners: dict[ Literal["daily", "hourly", "twice_daily"], - list[Callable[[list[dict[str, Any]] | None], None]], + list[Callable[[list[JsonValueType] | None], None]], ] _weather_option_temperature_unit: str | None = None @@ -789,9 +813,9 @@ class WeatherEntity(Entity): @final def _convert_forecast( self, native_forecast_list: list[Forecast] - ) -> list[dict[str, Any]]: + ) -> list[JsonValueType]: """Convert a forecast in native units to the unit configured by the user.""" - converted_forecast_list: list[dict[str, Any]] = [] + converted_forecast_list: list[JsonValueType] = [] precision = self.precision from_temp_unit = self.native_temperature_unit or self._default_temperature_unit @@ -1029,7 +1053,7 @@ class WeatherEntity(Entity): def async_subscribe_forecast( self, forecast_type: Literal["daily", "hourly", "twice_daily"], - forecast_listener: Callable[[list[dict[str, Any]] | None], None], + forecast_listener: Callable[[list[JsonValueType] | None], None], ) -> CALLBACK_TYPE: """Subscribe to forecast updates. @@ -1079,3 +1103,38 @@ class WeatherEntity(Entity): converted_forecast_list = self._convert_forecast(native_forecast_list) for listener in self._forecast_listeners[forecast_type]: listener(converted_forecast_list) + + +def raise_unsupported_forecast(entity_id: str, forecast_type: str) -> None: + """Raise error on attempt to get an unsupported forecast.""" + raise HomeAssistantError( + f"Weather entity '{entity_id}' does not support '{forecast_type}' forecast" + ) + + +async def async_get_forecast_service( + weather: WeatherEntity, service_call: ServiceCall +) -> ServiceResponse: + """Get weather forecast.""" + forecast_type = service_call.data["type"] + supported_features = weather.supported_features or 0 + if forecast_type == "daily": + if (supported_features & WeatherEntityFeature.FORECAST_DAILY) == 0: + raise_unsupported_forecast(weather.entity_id, forecast_type) + native_forecast_list = await weather.async_forecast_daily() + elif forecast_type == "hourly": + if (supported_features & WeatherEntityFeature.FORECAST_HOURLY) == 0: + raise_unsupported_forecast(weather.entity_id, forecast_type) + native_forecast_list = await weather.async_forecast_hourly() + else: + if (supported_features & WeatherEntityFeature.FORECAST_TWICE_DAILY) == 0: + raise_unsupported_forecast(weather.entity_id, forecast_type) + native_forecast_list = await weather.async_forecast_twice_daily() + if native_forecast_list is None: + converted_forecast_list = [] + else: + # pylint: disable-next=protected-access + converted_forecast_list = weather._convert_forecast(native_forecast_list) + return { + "forecast": converted_forecast_list, + } diff --git a/homeassistant/components/weather/services.yaml b/homeassistant/components/weather/services.yaml new file mode 100644 index 00000000000..b2b71396fab --- /dev/null +++ b/homeassistant/components/weather/services.yaml @@ -0,0 +1,18 @@ +get_forecast: + target: + entity: + domain: weather + supported_features: + - weather.WeatherEntityFeature.FORECAST_DAILY + - weather.WeatherEntityFeature.FORECAST_HOURLY + - weather.WeatherEntityFeature.FORECAST_TWICE_DAILY + fields: + type: + required: true + selector: + select: + options: + - "daily" + - "hourly" + - "twice_daily" + translation_key: forecast_type diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json index 21029c77284..5f08013684c 100644 --- a/homeassistant/components/weather/strings.json +++ b/homeassistant/components/weather/strings.json @@ -77,5 +77,26 @@ } } } + }, + "selector": { + "forecast_type": { + "options": { + "daily": "Daily", + "hourly": "Hourly", + "twice_daily": "Twice daily" + } + } + }, + "services": { + "get_forecast": { + "name": "Get forecast", + "description": "Get weather forecast.", + "fields": { + "type": { + "name": "Forecast type", + "description": "Forecast type: daily, hourly or twice daily." + } + } + } } } diff --git a/homeassistant/components/weather/websocket_api.py b/homeassistant/components/weather/websocket_api.py index f2be4dfec6d..39a487dcb2f 100644 --- a/homeassistant/components/weather/websocket_api.py +++ b/homeassistant/components/weather/websocket_api.py @@ -9,6 +9,7 @@ from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util.json import JsonValueType from .const import DOMAIN, VALID_UNITS, WeatherEntityFeature @@ -80,7 +81,7 @@ async def ws_subscribe_forecast( return @callback - def forecast_listener(forecast: list[dict[str, Any]] | None) -> None: + def forecast_listener(forecast: list[JsonValueType] | None) -> None: """Push a new forecast to websocket.""" connection.send_message( websocket_api.event_message( diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 192777ae3be..ba473758121 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -100,6 +100,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: from homeassistant.components.update import UpdateEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.components.water_heater import WaterHeaterEntityFeature + from homeassistant.components.weather import WeatherEntityFeature return { "AlarmControlPanelEntityFeature": AlarmControlPanelEntityFeature, @@ -117,6 +118,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: "UpdateEntityFeature": UpdateEntityFeature, "VacuumEntityFeature": VacuumEntityFeature, "WaterHeaterEntityFeature": WaterHeaterEntityFeature, + "WeatherEntityFeature": WeatherEntityFeature, } diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 92643b616c9..d8636330b5e 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -1,5 +1,6 @@ """The test for weather entity.""" from datetime import datetime +from typing import Any import pytest @@ -31,7 +32,9 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, + DOMAIN, ROUNDING_PRECISION, + SERVICE_GET_FORECAST, Forecast, WeatherEntity, WeatherEntityFeature, @@ -53,6 +56,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1103,3 +1107,121 @@ async def test_forecast_twice_daily_missing_is_daytime( assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"} assert not msg["success"] assert msg["type"] == "result" + + +@pytest.mark.parametrize( + ("forecast_type", "supported_features", "extra"), + [ + ("daily", WeatherEntityFeature.FORECAST_DAILY, {}), + ("hourly", WeatherEntityFeature.FORECAST_HOURLY, {}), + ( + "twice_daily", + WeatherEntityFeature.FORECAST_TWICE_DAILY, + {"is_daytime": True}, + ), + ], +) +async def test_get_forecast( + hass: HomeAssistant, + enable_custom_integrations: None, + forecast_type: str, + supported_features: int, + extra: dict[str, Any], +) -> None: + """Test get forecast service.""" + + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + supported_features=supported_features, + ) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity0.entity_id, + "type": forecast_type, + }, + blocking=True, + return_response=True, + ) + assert response == { + "forecast": [ + { + "cloud_coverage": None, + "temperature": 38.0, + "templow": 38.0, + "uv_index": None, + "wind_bearing": None, + } + | extra + ], + } + + +async def test_get_forecast_no_forecast( + hass: HomeAssistant, + enable_custom_integrations: None, +) -> None: + """Test get forecast service.""" + + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + supported_features=WeatherEntityFeature.FORECAST_DAILY, + ) + + entity0.forecast_list = None + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity0.entity_id, + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == { + "forecast": [], + } + + +@pytest.mark.parametrize( + ("supported_features", "forecast_types"), + [ + (WeatherEntityFeature.FORECAST_DAILY, ["hourly", "twice_daily"]), + (WeatherEntityFeature.FORECAST_HOURLY, ["daily", "twice_daily"]), + (WeatherEntityFeature.FORECAST_TWICE_DAILY, ["daily", "hourly"]), + ], +) +async def test_get_forecast_unsupported( + hass: HomeAssistant, + enable_custom_integrations: None, + forecast_types: list[str], + supported_features: int, +) -> None: + """Test get forecast service.""" + + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + supported_features=supported_features, + ) + + for forecast_type in forecast_types: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity0.entity_id, + "type": forecast_type, + }, + blocking=True, + return_response=True, + ) From 3a0822e03b939119c9f10a87cefab58ea42a5c2a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Aug 2023 14:06:16 +0200 Subject: [PATCH 009/179] Modernize met.no weather (#97952) --- homeassistant/components/met/config_flow.py | 2 +- homeassistant/components/met/weather.py | 33 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index d8cb31077c2..d36a9e58eb7 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -33,7 +33,7 @@ from .const import ( @callback def configured_instances(hass: HomeAssistant) -> set[str]: - """Return a set of configured SimpliSafe instances.""" + """Return a set of configured met.no instances.""" entries = [] for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("track_home"): diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 20822dc9973..d364066ae61 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -16,6 +16,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -27,7 +28,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -82,6 +83,9 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__( self, @@ -133,6 +137,15 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Return if the entity should be enabled when first added to the entity registry.""" return not self._hourly + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + super()._handle_coordinator_update() + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + self.hass, self.async_update_listeners(("daily", "hourly")) + ) + @property def condition(self) -> str | None: """Return the current condition.""" @@ -190,10 +203,9 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ATTR_MAP[ATTR_WEATHER_CLOUD_COVERAGE] ) - @property - def forecast(self) -> list[Forecast] | None: + def _forecast(self, hourly: bool) -> list[Forecast] | None: """Return the forecast array.""" - if self._hourly: + if hourly: met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast @@ -214,6 +226,19 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ha_forecast.append(ha_item) # type: ignore[arg-type] return ha_forecast + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast array.""" + return self._forecast(self._hourly) + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self._forecast(False) + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + return self._forecast(True) + @property def device_info(self) -> DeviceInfo: """Device info.""" From 65365d1db57a5e8cdf58d925c6e52871eb75f6be Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Mon, 7 Aug 2023 15:59:46 +0200 Subject: [PATCH 010/179] Integration tado bump (#97791) * Bumping to PyTado 0.16 and adding test coverage * Removing comment * Upgrading the deprecated functions * Updating tests to support paramterization * Delete test_config_flow.py Reverting the tests, which will be placed in a different PR. * Revert "Delete test_config_flow.py" This reverts commit 1719ebc990a32d3309f241f8adc8262008ca4ff3. * Reverting back changes --- homeassistant/components/tado/__init__.py | 39 ++++++++++----------- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 1cd21634c8e..b57d384124c 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -163,12 +163,11 @@ class TadoConnector: def setup(self): """Connect to Tado and fetch the zones.""" - self.tado = Tado(self._username, self._password) - self.tado.setDebugging(True) + self.tado = Tado(self._username, self._password, None, True) # Load zones and devices - self.zones = self.tado.getZones() - self.devices = self.tado.getDevices() - tado_home = self.tado.getMe()["homes"][0] + self.zones = self.tado.get_zones() + self.devices = self.tado.get_devices() + tado_home = self.tado.get_me()["homes"][0] self.home_id = tado_home["id"] self.home_name = tado_home["name"] @@ -181,7 +180,7 @@ class TadoConnector: def update_devices(self): """Update the device data from Tado.""" - devices = self.tado.getDevices() + devices = self.tado.get_devices() for device in devices: device_short_serial_no = device["shortSerialNo"] _LOGGER.debug("Updating device %s", device_short_serial_no) @@ -190,7 +189,7 @@ class TadoConnector: INSIDE_TEMPERATURE_MEASUREMENT in device["characteristics"]["capabilities"] ): - device[TEMP_OFFSET] = self.tado.getDeviceInfo( + device[TEMP_OFFSET] = self.tado.get_device_info( device_short_serial_no, TEMP_OFFSET ) except RuntimeError: @@ -218,7 +217,7 @@ class TadoConnector: def update_zones(self): """Update the zone data from Tado.""" try: - zone_states = self.tado.getZoneStates()["zoneStates"] + zone_states = self.tado.get_zone_states()["zoneStates"] except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zones") return @@ -230,7 +229,7 @@ class TadoConnector: """Update the internal data from Tado.""" _LOGGER.debug("Updating zone %s", zone_id) try: - data = self.tado.getZoneState(zone_id) + data = self.tado.get_zone_state(zone_id) except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id) return @@ -251,8 +250,8 @@ class TadoConnector: def update_home(self): """Update the home data from Tado.""" try: - self.data["weather"] = self.tado.getWeather() - self.data["geofence"] = self.tado.getHomeState() + self.data["weather"] = self.tado.get_weather() + self.data["geofence"] = self.tado.get_home_state() dispatcher_send( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), @@ -265,15 +264,15 @@ class TadoConnector: def get_capabilities(self, zone_id): """Return the capabilities of the devices.""" - return self.tado.getCapabilities(zone_id) + return self.tado.get_capabilities(zone_id) def get_auto_geofencing_supported(self): """Return whether the Tado Home supports auto geofencing.""" - return self.tado.getAutoGeofencingSupported() + return self.tado.get_auto_geofencing_supported() def reset_zone_overlay(self, zone_id): """Reset the zone back to the default operation.""" - self.tado.resetZoneOverlay(zone_id) + self.tado.reset_zone_overlay(zone_id) self.update_zone(zone_id) def set_presence( @@ -282,11 +281,11 @@ class TadoConnector: ): """Set the presence to home, away or auto.""" if presence == PRESET_AWAY: - self.tado.setAway() + self.tado.set_away() elif presence == PRESET_HOME: - self.tado.setHome() + self.tado.set_home() elif presence == PRESET_AUTO: - self.tado.setAuto() + self.tado.set_auto() # Update everything when changing modes self.update_zones() @@ -320,7 +319,7 @@ class TadoConnector: ) try: - self.tado.setZoneOverlay( + self.tado.set_zone_overlay( zone_id, overlay_mode, temperature, @@ -340,7 +339,7 @@ class TadoConnector: def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" try: - self.tado.setZoneOverlay( + self.tado.set_zone_overlay( zone_id, overlay_mode, None, None, device_type, "OFF" ) except RequestException as exc: @@ -351,6 +350,6 @@ class TadoConnector: def set_temperature_offset(self, device_id, offset): """Set temperature offset of device.""" try: - self.tado.setTempOffset(device_id, offset) + self.tado.set_temp_offset(device_id, offset) except RequestException as exc: _LOGGER.error("Could not set temperature offset: %s", exc) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 62f7a377239..bea608514bd 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.15.0"] + "requirements": ["python-tado==0.16.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index df1ab0595ea..b9933b28105 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2162,7 +2162,7 @@ python-smarttub==0.0.33 python-songpal==0.15.2 # homeassistant.components.tado -python-tado==0.15.0 +python-tado==0.16.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 891929c5391..f8dece4fb7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1591,7 +1591,7 @@ python-smarttub==0.0.33 python-songpal==0.15.2 # homeassistant.components.tado -python-tado==0.15.0 +python-tado==0.16.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 From 15eed166ec311bccc05f7a66f21fc1c5e5fe15ea Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Aug 2023 17:24:43 +0200 Subject: [PATCH 011/179] Modernize SMHI weather (#97275) * SMHI forecast service * Mod weather * reset weather * Fix tests * coverage * add test --- homeassistant/components/smhi/weather.py | 79 +- tests/components/smhi/conftest.py | 10 +- tests/components/smhi/fixtures/smhi.json | 10084 ++++++++++++++++ .../components/smhi/fixtures/smhi_short.json | 148 + .../smhi/snapshots/test_weather.ambr | 426 + tests/components/smhi/test_weather.py | 190 +- tests/fixtures/smhi.json | 1252 -- 7 files changed, 10884 insertions(+), 1305 deletions(-) create mode 100644 tests/components/smhi/fixtures/smhi.json create mode 100644 tests/components/smhi/fixtures/smhi_short.json create mode 100644 tests/components/smhi/snapshots/test_weather.ambr delete mode 100644 tests/fixtures/smhi.json diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index ece0e4f6d5c..6f50f5c9a65 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -40,6 +40,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_BEARING, Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -128,6 +129,9 @@ class SmhiWeather(WeatherEntity): _attr_has_entity_name = True _attr_name = None + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__( self, @@ -138,7 +142,8 @@ class SmhiWeather(WeatherEntity): ) -> None: """Initialize the SMHI weather entity.""" self._attr_unique_id = f"{latitude}, {longitude}" - self._forecasts: list[SmhiForecast] | None = None + self._forecast_daily: list[SmhiForecast] | None = None + self._forecast_hourly: list[SmhiForecast] | None = None self._fail_count = 0 self._smhi_api = Smhi(longitude, latitude, session=session) self._attr_device_info = DeviceInfo( @@ -155,9 +160,9 @@ class SmhiWeather(WeatherEntity): @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional attributes.""" - if self._forecasts: + if self._forecast_daily: return { - ATTR_SMHI_THUNDER_PROBABILITY: self._forecasts[0].thunder, + ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily[0].thunder, } return None @@ -166,7 +171,8 @@ class SmhiWeather(WeatherEntity): """Refresh the forecast data from SMHI weather API.""" try: async with async_timeout.timeout(TIMEOUT): - self._forecasts = await self._smhi_api.async_get_forecast() + self._forecast_daily = await self._smhi_api.async_get_forecast() + self._forecast_hourly = await self._smhi_api.async_get_forecast_hour() self._fail_count = 0 except (asyncio.TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") @@ -175,23 +181,24 @@ class SmhiWeather(WeatherEntity): async_call_later(self.hass, RETRY_TIMEOUT, self.retry_update) return - if self._forecasts: - self._attr_native_temperature = self._forecasts[0].temperature - self._attr_humidity = self._forecasts[0].humidity - self._attr_native_wind_speed = self._forecasts[0].wind_speed - self._attr_wind_bearing = self._forecasts[0].wind_direction - self._attr_native_visibility = self._forecasts[0].horizontal_visibility - self._attr_native_pressure = self._forecasts[0].pressure - self._attr_native_wind_gust_speed = self._forecasts[0].wind_gust - self._attr_cloud_coverage = self._forecasts[0].cloudiness + if self._forecast_daily: + self._attr_native_temperature = self._forecast_daily[0].temperature + self._attr_humidity = self._forecast_daily[0].humidity + self._attr_native_wind_speed = self._forecast_daily[0].wind_speed + self._attr_wind_bearing = self._forecast_daily[0].wind_direction + self._attr_native_visibility = self._forecast_daily[0].horizontal_visibility + self._attr_native_pressure = self._forecast_daily[0].pressure + self._attr_native_wind_gust_speed = self._forecast_daily[0].wind_gust + self._attr_cloud_coverage = self._forecast_daily[0].cloudiness self._attr_condition = next( ( k for k, v in CONDITION_CLASSES.items() - if self._forecasts[0].symbol in v + if self._forecast_daily[0].symbol in v ), None, ) + await self.async_update_listeners(("daily", "hourly")) async def retry_update(self, _: datetime) -> None: """Retry refresh weather forecast.""" @@ -202,12 +209,12 @@ class SmhiWeather(WeatherEntity): @property def forecast(self) -> list[Forecast] | None: """Return the forecast.""" - if self._forecasts is None or len(self._forecasts) < 2: + if self._forecast_daily is None or len(self._forecast_daily) < 2: return None data: list[Forecast] = [] - for forecast in self._forecasts[1:]: + for forecast in self._forecast_daily[1:]: condition = next( (k for k, v in CONDITION_CLASSES.items() if forecast.symbol in v), None ) @@ -229,3 +236,43 @@ class SmhiWeather(WeatherEntity): ) return data + + def _get_forecast_data( + self, forecast_data: list[SmhiForecast] | None + ) -> list[Forecast] | None: + """Get forecast data.""" + if forecast_data is None or len(forecast_data) < 3: + return None + + data: list[Forecast] = [] + + for forecast in forecast_data[1:]: + condition = next( + (k for k, v in CONDITION_CLASSES.items() if forecast.symbol in v), None + ) + + data.append( + { + ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), + ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max, + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min, + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation, + ATTR_FORECAST_CONDITION: condition, + ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure, + ATTR_FORECAST_WIND_BEARING: forecast.wind_direction, + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed, + ATTR_FORECAST_HUMIDITY: forecast.humidity, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast.wind_gust, + ATTR_FORECAST_CLOUD_COVERAGE: forecast.cloudiness, + } + ) + + return data + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Service to retrieve the daily forecast.""" + return self._get_forecast_data(self._forecast_daily) + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Service to retrieve the hourly forecast.""" + return self._get_forecast_data(self._forecast_hourly) diff --git a/tests/components/smhi/conftest.py b/tests/components/smhi/conftest.py index 6ededa6d975..c474bc50b51 100644 --- a/tests/components/smhi/conftest.py +++ b/tests/components/smhi/conftest.py @@ -1,10 +1,18 @@ """Provide common smhi fixtures.""" import pytest +from homeassistant.components.smhi.const import DOMAIN + from tests.common import load_fixture @pytest.fixture(scope="session") def api_response(): """Return an API response.""" - return load_fixture("smhi.json") + return load_fixture("smhi.json", DOMAIN) + + +@pytest.fixture(scope="session") +def api_response_lack_data(): + """Return an API response.""" + return load_fixture("smhi_short.json", DOMAIN) diff --git a/tests/components/smhi/fixtures/smhi.json b/tests/components/smhi/fixtures/smhi.json new file mode 100644 index 00000000000..35770ddd355 --- /dev/null +++ b/tests/components/smhi/fixtures/smhi.json @@ -0,0 +1,10084 @@ +{ + "approvedTime": "2023-08-07T07:07:34Z", + "referenceTime": "2023-08-07T07:00:00Z", + "geometry": { + "type": "Point", + "coordinates": [[15.990068, 57.997072]] + }, + "timeSeries": [ + { + "validTime": "2023-08-07T08:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [18.4] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [992.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [0.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [93] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [37] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [7] + } + ] + }, + { + "validTime": "2023-08-07T09:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [18.2] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [992.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [0.1] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [103] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.7] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [27] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.6] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [7] + } + ] + }, + { + "validTime": "2023-08-07T10:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.5] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [992.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [104] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.7] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [27] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.6] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T11:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.6] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [992.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [109] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.6] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T12:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.1] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [114] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [96] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T13:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.7] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [7.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [105] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [91] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [8.8] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T14:00:00Z", + "parameters": [ + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.2] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.5] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [10.7] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [99] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [86] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T15:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [16.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [9.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [108] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.4] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [89] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [8.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T16:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.4] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [16.5] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [11.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [113] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.7] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [84] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [10.1] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T17:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.4] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [16.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [9.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [100] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [88] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T18:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [990.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [15.6] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [7.7] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [107] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [91] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T19:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [990.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [15.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [7.3] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [88] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.2] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [94] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T20:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [989.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [15.0] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [4.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [39] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T21:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [989.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [66] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.3] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [98] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T22:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [989.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.1] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [81] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.4] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [98] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-07T23:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [988.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [15.0] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [81] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.2] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-08T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [987.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [357] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [99] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.9] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-08T01:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [986.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [5] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.6] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [99] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T02:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [985.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [359] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.4] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T03:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [985.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.3] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [293] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [2] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T04:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [984.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [295] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [0.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [4] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.7] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T05:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [984.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [0.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [221] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [5] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.4] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.7] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T06:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [983.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.7] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [230] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [1.9] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [5] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T07:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [983.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.5] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [209] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.4] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [98] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [6] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.9] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T08:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [983.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [197] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [98] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [6] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T09:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [983.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [192] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.7] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [98] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [6] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T10:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [983.4] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [184] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [6] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T11:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [983.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [181] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [4] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [984.1] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.7] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [183] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [3] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T13:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [984.4] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.6] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [190] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [96] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [2] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T14:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [985.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [205] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.3] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [96] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [10.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-08T15:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [985.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.9] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [211] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [96] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [11.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T16:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [986.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [213] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.7] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [11.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.2] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T17:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [987.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [209] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [96] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [11.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.3] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T18:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [989.1] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [208] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.6] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.9] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.3] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T19:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [990.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [10.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [4.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [203] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.2] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T20:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [991.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [10.6] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [4.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [201] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T21:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [992.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [10.6] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [187] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.4] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.9] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.7] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.7] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T22:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [993.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [10.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.1] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [185] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [96] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.9] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-08T23:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [994.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [10.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [8.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [192] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [90] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-09T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [995.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [11.1] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [193] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [85] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T01:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [996.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.3] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [12.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [188] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [82] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T02:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [996.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [13.3] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [189] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [81] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T03:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [997.4] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [14.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [187] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.6] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [78] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [11.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T04:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [998.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [13.9] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [171] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [80] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [11.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T05:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [998.9] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [13.7] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [167] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [80] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [11.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T06:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [999.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [14.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [165] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.6] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [80] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.1] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T07:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [999.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [15.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [166] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.7] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [77] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T08:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1000.1] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.3] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [16.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [169] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.9] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [75] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-09T09:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1000.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.5] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [24.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [167] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [77] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-09T10:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1000.7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [18.7] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [164] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.9] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [89] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-09T11:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1001.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [8.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [159] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.9] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [94] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [12.1] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.3] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.4] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-09T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1001.4] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [3.1] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [166] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [95] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [13.4] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.5] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-09T18:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1006.2] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.0] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [199] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.2] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [99] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [5] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-10T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1007.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [10.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [1.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [200] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [99] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.5] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.7] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-10T06:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1009.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.0] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [182] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.3] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [19] + } + ] + }, + { + "validTime": "2023-08-10T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1011.1] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.9] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [30.9] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [174] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [75] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [8.1] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-10T18:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1011.9] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [43.1] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [143] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [89] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [2] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [18] + } + ] + }, + { + "validTime": "2023-08-11T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1012.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [11.7] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [169] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [98] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [4] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-11T06:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1013.5] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.3] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [214] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.2] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [4] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-11T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1015.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.6] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [27.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [197] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [69] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [4] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-11T18:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1015.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [16.1] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [35.3] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [156] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.3] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [82] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [1] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.7] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [8] + } + ] + }, + { + "validTime": "2023-08-12T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1015.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.3] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [2.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [191] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.4] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [97] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [1] + } + ] + }, + { + "validTime": "2023-08-12T06:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1014.8] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [12.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [40.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [171] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [92] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-12T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1014.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [17.0] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [50.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [225] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.4] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [82] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [7.8] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-13T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1013.9] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.6] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [31.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [233] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [92] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.6] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [2] + } + ] + }, + { + "validTime": "2023-08-13T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1013.6] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [20.0] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [46.8] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [234] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [4.1] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [59] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.9] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + } + ] + }, + { + "validTime": "2023-08-14T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1015.2] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.5] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [37.2] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [227] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.0] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [91] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.5] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.6] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + } + ] + }, + { + "validTime": "2023-08-14T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1015.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [20.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [49.0] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [216] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [56] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [4] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.4] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + } + ] + }, + { + "validTime": "2023-08-15T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1014.9] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [14.3] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [30.3] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [196] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [93] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [6] + } + ] + }, + { + "validTime": "2023-08-15T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1014.3] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [20.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [39.9] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [226] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [64] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.2] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.0] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.2] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + } + ] + }, + { + "validTime": "2023-08-16T00:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1014.9] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [13.8] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [31.6] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [228] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.8] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [93] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [3] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [0] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [5.9] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.8] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [2] + } + ] + }, + { + "validTime": "2023-08-16T12:00:00Z", + "parameters": [ + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [1014.0] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [20.2] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [44.5] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [233] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [3.9] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [61] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [6] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [2] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [1] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [9.3] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [1.5] + }, + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [0] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [3] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.1] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [4] + } + ] + } + ] +} diff --git a/tests/components/smhi/fixtures/smhi_short.json b/tests/components/smhi/fixtures/smhi_short.json new file mode 100644 index 00000000000..ad9567b7f57 --- /dev/null +++ b/tests/components/smhi/fixtures/smhi_short.json @@ -0,0 +1,148 @@ +{ + "approvedTime": "2023-08-07T07:07:34Z", + "referenceTime": "2023-08-07T07:00:00Z", + "geometry": { + "type": "Point", + "coordinates": [[15.990068, 57.997072]] + }, + "timeSeries": [ + { + "validTime": "2023-08-07T08:00:00Z", + "parameters": [ + { + "name": "spp", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [-9] + }, + { + "name": "pcat", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [0] + }, + { + "name": "pmin", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmean", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmax", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "pmedian", + "levelType": "hl", + "level": 0, + "unit": "kg/m2/h", + "values": [0.0] + }, + { + "name": "tcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "lcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [8] + }, + { + "name": "mcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "hcc_mean", + "levelType": "hl", + "level": 0, + "unit": "octas", + "values": [7] + }, + { + "name": "t", + "levelType": "hl", + "level": 2, + "unit": "Cel", + "values": [18.4] + }, + { + "name": "msl", + "levelType": "hmsl", + "level": 0, + "unit": "hPa", + "values": [992.4] + }, + { + "name": "vis", + "levelType": "hl", + "level": 2, + "unit": "km", + "values": [0.4] + }, + { + "name": "wd", + "levelType": "hl", + "level": 10, + "unit": "degree", + "values": [93] + }, + { + "name": "ws", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [2.5] + }, + { + "name": "r", + "levelType": "hl", + "level": 2, + "unit": "percent", + "values": [100] + }, + { + "name": "tstm", + "levelType": "hl", + "level": 0, + "unit": "percent", + "values": [37] + }, + { + "name": "gust", + "levelType": "hl", + "level": 10, + "unit": "m/s", + "values": [6.2] + }, + { + "name": "Wsymb2", + "levelType": "hl", + "level": 0, + "unit": "category", + "values": [7] + } + ] + } + ] +} diff --git a/tests/components/smhi/snapshots/test_weather.ambr b/tests/components/smhi/snapshots/test_weather.ambr new file mode 100644 index 00000000000..ade151ed128 --- /dev/null +++ b/tests/components/smhi/snapshots/test_weather.ambr @@ -0,0 +1,426 @@ +# serializer version: 1 +# name: test_forecast_daily + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-07T12:00:00', + 'humidity': 96, + 'precipitation': 0.0, + 'pressure': 991.0, + 'temperature': 18.0, + 'templow': 15.0, + 'wind_bearing': 114, + 'wind_gust_speed': 32.76, + 'wind_speed': 10.08, + }) +# --- +# name: test_forecast_daily.1 + dict({ + 'cloud_coverage': 75, + 'condition': 'partlycloudy', + 'datetime': '2023-08-13T12:00:00', + 'humidity': 59, + 'precipitation': 0.0, + 'pressure': 1013.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 234, + 'wind_gust_speed': 35.64, + 'wind_speed': 14.76, + }) +# --- +# name: test_forecast_daily.2 + dict({ + 'cloud_coverage': 100, + 'condition': 'fog', + 'datetime': '2023-08-07T09:00:00', + 'humidity': 100, + 'precipitation': 0.0, + 'pressure': 992.0, + 'temperature': 18.0, + 'templow': 18.0, + 'wind_bearing': 103, + 'wind_gust_speed': 23.76, + 'wind_speed': 9.72, + }) +# --- +# name: test_forecast_daily.3 + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-07T15:00:00', + 'humidity': 89, + 'precipitation': 0.0, + 'pressure': 991.0, + 'temperature': 16.0, + 'templow': 16.0, + 'wind_bearing': 108, + 'wind_gust_speed': 31.68, + 'wind_speed': 12.24, + }) +# --- +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-07T12:00:00', + 'humidity': 96, + 'precipitation': 0.0, + 'pressure': 991.0, + 'temperature': 18.0, + 'templow': 15.0, + 'wind_bearing': 114, + 'wind_gust_speed': 32.76, + 'wind_speed': 10.08, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'rainy', + 'datetime': '2023-08-08T12:00:00', + 'humidity': 97, + 'precipitation': 10.6, + 'pressure': 984.0, + 'temperature': 15.0, + 'templow': 11.0, + 'wind_bearing': 183, + 'wind_gust_speed': 27.36, + 'wind_speed': 11.16, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'rainy', + 'datetime': '2023-08-09T12:00:00', + 'humidity': 95, + 'precipitation': 6.3, + 'pressure': 1001.0, + 'temperature': 12.0, + 'templow': 11.0, + 'wind_bearing': 166, + 'wind_gust_speed': 48.24, + 'wind_speed': 18.0, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-10T12:00:00', + 'humidity': 75, + 'precipitation': 4.8, + 'pressure': 1011.0, + 'temperature': 14.0, + 'templow': 10.0, + 'wind_bearing': 174, + 'wind_gust_speed': 29.16, + 'wind_speed': 11.16, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-11T12:00:00', + 'humidity': 69, + 'precipitation': 0.6, + 'pressure': 1015.0, + 'temperature': 18.0, + 'templow': 12.0, + 'wind_bearing': 197, + 'wind_gust_speed': 27.36, + 'wind_speed': 10.08, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-12T12:00:00', + 'humidity': 82, + 'precipitation': 0.0, + 'pressure': 1014.0, + 'temperature': 17.0, + 'templow': 12.0, + 'wind_bearing': 225, + 'wind_gust_speed': 28.08, + 'wind_speed': 8.64, + }), + dict({ + 'cloud_coverage': 75, + 'condition': 'partlycloudy', + 'datetime': '2023-08-13T12:00:00', + 'humidity': 59, + 'precipitation': 0.0, + 'pressure': 1013.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 234, + 'wind_gust_speed': 35.64, + 'wind_speed': 14.76, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'partlycloudy', + 'datetime': '2023-08-14T12:00:00', + 'humidity': 56, + 'precipitation': 0.0, + 'pressure': 1015.0, + 'temperature': 21.0, + 'templow': 14.0, + 'wind_bearing': 216, + 'wind_gust_speed': 33.12, + 'wind_speed': 13.68, + }), + dict({ + 'cloud_coverage': 88, + 'condition': 'partlycloudy', + 'datetime': '2023-08-15T12:00:00', + 'humidity': 64, + 'precipitation': 3.6, + 'pressure': 1014.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 226, + 'wind_gust_speed': 33.12, + 'wind_speed': 13.68, + }), + dict({ + 'cloud_coverage': 75, + 'condition': 'partlycloudy', + 'datetime': '2023-08-16T12:00:00', + 'humidity': 61, + 'precipitation': 2.4, + 'pressure': 1014.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 233, + 'wind_gust_speed': 33.48, + 'wind_speed': 14.04, + }), + ]), + }) +# --- +# name: test_forecast_services + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-07T12:00:00', + 'humidity': 96, + 'precipitation': 0.0, + 'pressure': 991.0, + 'temperature': 18.0, + 'templow': 15.0, + 'wind_bearing': 114, + 'wind_gust_speed': 32.76, + 'wind_speed': 10.08, + }) +# --- +# name: test_forecast_services.1 + dict({ + 'cloud_coverage': 75, + 'condition': 'partlycloudy', + 'datetime': '2023-08-13T12:00:00', + 'humidity': 59, + 'precipitation': 0.0, + 'pressure': 1013.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 234, + 'wind_gust_speed': 35.64, + 'wind_speed': 14.76, + }) +# --- +# name: test_forecast_services.2 + dict({ + 'cloud_coverage': 100, + 'condition': 'fog', + 'datetime': '2023-08-07T09:00:00', + 'humidity': 100, + 'precipitation': 0.0, + 'pressure': 992.0, + 'temperature': 18.0, + 'templow': 18.0, + 'wind_bearing': 103, + 'wind_gust_speed': 23.76, + 'wind_speed': 9.72, + }) +# --- +# name: test_forecast_services.3 + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-07T15:00:00', + 'humidity': 89, + 'precipitation': 0.0, + 'pressure': 991.0, + 'temperature': 16.0, + 'templow': 16.0, + 'wind_bearing': 108, + 'wind_gust_speed': 31.68, + 'wind_speed': 12.24, + }) +# --- +# name: test_setup_hass + ReadOnlyDict({ + 'apparent_temperature': 18.0, + 'attribution': 'Swedish weather institute (SMHI)', + 'cloud_coverage': 100, + 'forecast': list([ + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-07T12:00:00', + 'humidity': 96, + 'precipitation': 0.0, + 'pressure': 991.0, + 'temperature': 18.0, + 'templow': 15.0, + 'wind_bearing': 114, + 'wind_gust_speed': 32.76, + 'wind_speed': 10.08, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'rainy', + 'datetime': '2023-08-08T12:00:00', + 'humidity': 97, + 'precipitation': 10.6, + 'pressure': 984.0, + 'temperature': 15.0, + 'templow': 11.0, + 'wind_bearing': 183, + 'wind_gust_speed': 27.36, + 'wind_speed': 11.16, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'rainy', + 'datetime': '2023-08-09T12:00:00', + 'humidity': 95, + 'precipitation': 6.3, + 'pressure': 1001.0, + 'temperature': 12.0, + 'templow': 11.0, + 'wind_bearing': 166, + 'wind_gust_speed': 48.24, + 'wind_speed': 18.0, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-10T12:00:00', + 'humidity': 75, + 'precipitation': 4.8, + 'pressure': 1011.0, + 'temperature': 14.0, + 'templow': 10.0, + 'wind_bearing': 174, + 'wind_gust_speed': 29.16, + 'wind_speed': 11.16, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-11T12:00:00', + 'humidity': 69, + 'precipitation': 0.6, + 'pressure': 1015.0, + 'temperature': 18.0, + 'templow': 12.0, + 'wind_bearing': 197, + 'wind_gust_speed': 27.36, + 'wind_speed': 10.08, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2023-08-12T12:00:00', + 'humidity': 82, + 'precipitation': 0.0, + 'pressure': 1014.0, + 'temperature': 17.0, + 'templow': 12.0, + 'wind_bearing': 225, + 'wind_gust_speed': 28.08, + 'wind_speed': 8.64, + }), + dict({ + 'cloud_coverage': 75, + 'condition': 'partlycloudy', + 'datetime': '2023-08-13T12:00:00', + 'humidity': 59, + 'precipitation': 0.0, + 'pressure': 1013.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 234, + 'wind_gust_speed': 35.64, + 'wind_speed': 14.76, + }), + dict({ + 'cloud_coverage': 100, + 'condition': 'partlycloudy', + 'datetime': '2023-08-14T12:00:00', + 'humidity': 56, + 'precipitation': 0.0, + 'pressure': 1015.0, + 'temperature': 21.0, + 'templow': 14.0, + 'wind_bearing': 216, + 'wind_gust_speed': 33.12, + 'wind_speed': 13.68, + }), + dict({ + 'cloud_coverage': 88, + 'condition': 'partlycloudy', + 'datetime': '2023-08-15T12:00:00', + 'humidity': 64, + 'precipitation': 3.6, + 'pressure': 1014.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 226, + 'wind_gust_speed': 33.12, + 'wind_speed': 13.68, + }), + dict({ + 'cloud_coverage': 75, + 'condition': 'partlycloudy', + 'datetime': '2023-08-16T12:00:00', + 'humidity': 61, + 'precipitation': 2.4, + 'pressure': 1014.0, + 'temperature': 20.0, + 'templow': 14.0, + 'wind_bearing': 233, + 'wind_gust_speed': 33.48, + 'wind_speed': 14.04, + }), + ]), + 'friendly_name': 'test', + 'humidity': 100, + 'precipitation_unit': , + 'pressure': 992.0, + 'pressure_unit': , + 'supported_features': , + 'temperature': 18.0, + 'temperature_unit': , + 'thunder_probability': 37, + 'visibility': 0.4, + 'visibility_unit': , + 'wind_bearing': 93, + 'wind_gust_speed': 22.32, + 'wind_speed': 9.0, + 'wind_speed_unit': , + }) +# --- +# name: test_setup_hass.1 + dict({ + 'cloud_coverage': 100, + 'condition': 'rainy', + 'datetime': '2023-08-08T12:00:00', + 'humidity': 97, + 'precipitation': 10.6, + 'pressure': 984.0, + 'temperature': 15.0, + 'templow': 11.0, + 'wind_bearing': 183, + 'wind_gust_speed': 27.36, + 'wind_speed': 11.16, + }) +# --- diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 55b07530c39..a2628b11b84 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -5,21 +5,16 @@ from unittest.mock import patch import pytest from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smhi.const import ATTR_SMHI_THUNDER_PROBABILITY -from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEOUT +from homeassistant.components.smhi.weather import ( + CONDITION_CLASSES, + RETRY_TIMEOUT, +) from homeassistant.components.weather import ( ATTR_FORECAST, - ATTR_FORECAST_CLOUD_COVERAGE, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_GUST_SPEED, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -28,6 +23,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, ) from homeassistant.components.weather.const import ( ATTR_WEATHER_CLOUD_COVERAGE, @@ -42,10 +38,14 @@ from . import ENTITY_ID, TEST_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import WebSocketGenerator async def test_setup_hass( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + api_response: str, + snapshot: SnapshotAssertion, ) -> None: """Test for successfully setting up the smhi integration.""" uri = APIURL_TEMPLATE.format( @@ -58,37 +58,19 @@ async def test_setup_hass( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 # Testing the actual entity state for # deeper testing than normal unity test state = hass.states.get(ENTITY_ID) assert state - assert state.state == "sunny" - assert state.attributes[ATTR_WEATHER_CLOUD_COVERAGE] == 50 - assert state.attributes[ATTR_SMHI_THUNDER_PROBABILITY] == 33 - assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 16.92 - assert state.attributes[ATTR_ATTRIBUTION].find("SMHI") >= 0 - assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55 - assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024 - assert state.attributes[ATTR_WEATHER_TEMPERATURE] == 17 - assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50 - assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 6.84 - assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134 - assert len(state.attributes["forecast"]) == 4 + assert state.state == "fog" + assert state.attributes == snapshot + assert len(state.attributes["forecast"]) == 10 forecast = state.attributes["forecast"][1] - assert forecast[ATTR_FORECAST_TIME] == "2018-09-02T12:00:00" - assert forecast[ATTR_FORECAST_TEMP] == 21 - assert forecast[ATTR_FORECAST_TEMP_LOW] == 6 - assert forecast[ATTR_FORECAST_PRECIPITATION] == 0 - assert forecast[ATTR_FORECAST_CONDITION] == "partlycloudy" - assert forecast[ATTR_FORECAST_PRESSURE] == 1026 - assert forecast[ATTR_FORECAST_WIND_BEARING] == 203 - assert forecast[ATTR_FORECAST_WIND_SPEED] == 6.12 - assert forecast[ATTR_FORECAST_WIND_GUST_SPEED] == 18.36 - assert forecast[ATTR_FORECAST_CLOUD_COVERAGE] == 100 + assert forecast == snapshot async def test_properties_no_data(hass: HomeAssistant) -> None: @@ -188,6 +170,9 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: with patch( "homeassistant.components.smhi.weather.Smhi.async_get_forecast", return_value=testdata, + ), patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast_hour", + return_value=None, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -341,7 +326,7 @@ async def test_custom_speed_unit( assert state assert state.name == "test" - assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 16.92 + assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 22.32 entity_reg = er.async_get(hass) entity_reg.async_update_entity_options( @@ -353,4 +338,137 @@ async def test_custom_speed_unit( await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 4.7 + assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 6.2 + + +async def test_forecast_services( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + aioclient_mock: AiohttpClientMocker, + api_response: str, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) + aioclient_mock.get(uri, text=api_response) + + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "daily", + "entity_id": ENTITY_ID, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert len(forecast1) == 10 + assert forecast1[0] == snapshot + assert forecast1[6] == snapshot + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "hourly", + "entity_id": ENTITY_ID, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert len(forecast1) == 72 + assert forecast1[0] == snapshot + assert forecast1[6] == snapshot + + +async def test_forecast_services_lack_of_data( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + aioclient_mock: AiohttpClientMocker, + api_response_lack_data: str, + snapshot: SnapshotAssertion, +) -> None: + """Test forecast lacking data.""" + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) + aioclient_mock.get(uri, text=api_response_lack_data) + + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "daily", + "entity_id": ENTITY_ID, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert forecast1 is None + + +async def test_forecast_service( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + api_response: str, + snapshot: SnapshotAssertion, +) -> None: + """Test forecast service.""" + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) + aioclient_mock.get(uri, text=api_response) + + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": ENTITY_ID, "type": "daily"}, + blocking=True, + return_response=True, + ) + assert response == snapshot diff --git a/tests/fixtures/smhi.json b/tests/fixtures/smhi.json deleted file mode 100644 index e2da28534a0..00000000000 --- a/tests/fixtures/smhi.json +++ /dev/null @@ -1,1252 +0,0 @@ -{ - "approvedTime": "2018-09-01T14:06:18Z", - "referenceTime": "2018-09-01T14:00:00Z", - "geometry": { - "type": "Point", - "coordinates": [[16.024394, 63.341937]] - }, - "timeSeries": [ - { - "validTime": "2018-09-01T15:00:00Z", - "parameters": [ - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [1] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [2] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [1] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [4] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1024.6] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [17] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [134] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.9] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [55] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [33] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [4.7] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [1] - } - ] - }, - { - "validTime": "2018-09-02T00:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1026] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [6] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [12] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [214] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [0.7] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [87] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [0] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.5] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [1] - } - ] - }, - { - "validTime": "2018-09-02T11:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1026.6] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [19.8] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [201] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.8] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [43] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [0] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [6] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [6] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [5.2] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [3] - } - ] - }, - { - "validTime": "2018-09-02T12:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1026.5] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [20.6] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [203] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.7] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [43] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [0] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [9] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [6] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [5.1] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [3] - } - ] - }, - { - "validTime": "2018-09-02T23:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1026] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [9.3] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [19.4] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [95] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [0.5] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [75] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [1] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.1] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [1] - } - ] - }, - { - "validTime": "2018-09-03T00:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1025.9] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [8.5] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [104] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [0.5] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [73] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [1] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.1] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [1] - } - ] - }, - { - "validTime": "2018-09-03T01:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1025.6] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [8] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [116] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [0.3] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [74] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [1] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [1] - } - ] - }, - { - "validTime": "2018-09-04T12:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1020.5] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [19.2] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [353] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [1.4] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [60] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [0] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [7] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [3] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [5] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [4] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [4.7] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [-9] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [0] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [4] - } - ] - }, - { - "validTime": "2018-09-04T18:00:00Z", - "parameters": [ - { - "name": "msl", - "levelType": "hmsl", - "level": 0, - "unit": "hPa", - "values": [1021.5] - }, - { - "name": "t", - "levelType": "hl", - "level": 2, - "unit": "Cel", - "values": [14.3] - }, - { - "name": "vis", - "levelType": "hl", - "level": 2, - "unit": "km", - "values": [50] - }, - { - "name": "wd", - "levelType": "hl", - "level": 10, - "unit": "degree", - "values": [333] - }, - { - "name": "ws", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [2.3] - }, - { - "name": "r", - "levelType": "hl", - "level": 2, - "unit": "percent", - "values": [81] - }, - { - "name": "tstm", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [0] - }, - { - "name": "tcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [4] - }, - { - "name": "lcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [1] - }, - { - "name": "mcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [4] - }, - { - "name": "hcc_mean", - "levelType": "hl", - "level": 0, - "unit": "octas", - "values": [0] - }, - { - "name": "gust", - "levelType": "hl", - "level": 10, - "unit": "m/s", - "values": [4.5] - }, - { - "name": "pmin", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmax", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0.2] - }, - { - "name": "spp", - "levelType": "hl", - "level": 0, - "unit": "percent", - "values": [0] - }, - { - "name": "pcat", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [4] - }, - { - "name": "pmean", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "pmedian", - "levelType": "hl", - "level": 0, - "unit": "kg/m2/h", - "values": [0] - }, - { - "name": "Wsymb2", - "levelType": "hl", - "level": 0, - "unit": "category", - "values": [3] - } - ] - } - ] -} From c4da5374aeacac7458994e58f6b7f2c741c8ab47 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Aug 2023 17:25:02 +0200 Subject: [PATCH 012/179] Refactor Trafikverket Train to improve config flow (#97929) * Refactor tvt * review fixes * review comments --- .coveragerc | 1 + .../trafikverket_train/config_flow.py | 154 +++++++++----- .../trafikverket_train/coordinator.py | 28 +-- .../trafikverket_train/strings.json | 6 +- .../components/trafikverket_train/util.py | 25 ++- .../trafikverket_train/test_config_flow.py | 191 +++++++++++++++--- 6 files changed, 290 insertions(+), 115 deletions(-) diff --git a/.coveragerc b/.coveragerc index d895b1adf0a..01e1d0d3b0e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1341,6 +1341,7 @@ omit = homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/coordinator.py homeassistant/components/trafikverket_train/sensor.py + homeassistant/components/trafikverket_train/util.py homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index fc23d3b953d..f5000851755 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -2,18 +2,24 @@ from __future__ import annotations from collections.abc import Mapping +from datetime import datetime +import logging from typing import Any from pytrafikverket import TrafikverketTrain from pytrafikverket.exceptions import ( InvalidAuthentication, + MultipleTrainAnnouncementFound, MultipleTrainStationsFound, + NoTrainAnnouncementFound, NoTrainStationFound, + UnknownError, ) import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,18 +28,21 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, SelectSelectorMode, TextSelector, + TimeSelector, ) import homeassistant.util.dt as dt_util from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN -from .util import create_unique_id +from .util import create_unique_id, next_departuredate + +_LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): TextSelector(), vol.Required(CONF_FROM): TextSelector(), vol.Required(CONF_TO): TextSelector(), - vol.Optional(CONF_TIME): TextSelector(), + vol.Optional(CONF_TIME): TimeSelector(), vol.Required(CONF_WEEKDAY, default=WEEKDAYS): SelectSelector( SelectSelectorConfig( options=WEEKDAYS, @@ -51,6 +60,56 @@ DATA_SCHEMA_REAUTH = vol.Schema( ) +async def validate_input( + hass: HomeAssistant, + api_key: str, + train_from: str, + train_to: str, + train_time: str | None, + weekdays: list[str], +) -> dict[str, str]: + """Validate input from user input.""" + errors: dict[str, str] = {} + + when = dt_util.now() + if train_time: + departure_day = next_departuredate(weekdays) + if _time := dt_util.parse_time(train_time): + when = datetime.combine( + departure_day, + _time, + dt_util.get_time_zone(hass.config.time_zone), + ) + + try: + web_session = async_get_clientsession(hass) + train_api = TrafikverketTrain(web_session, api_key) + from_station = await train_api.async_get_train_station(train_from) + to_station = await train_api.async_get_train_station(train_to) + if train_time: + await train_api.async_get_train_stop(from_station, to_station, when) + else: + await train_api.async_get_next_train_stop(from_station, to_station, when) + except InvalidAuthentication: + errors["base"] = "invalid_auth" + except NoTrainStationFound: + errors["base"] = "invalid_station" + except MultipleTrainStationsFound: + errors["base"] = "more_stations" + except NoTrainAnnouncementFound: + errors["base"] = "no_trains" + except MultipleTrainAnnouncementFound: + errors["base"] = "multiple_trains" + except UnknownError as error: + _LOGGER.error("Unknown error occurred during validation %s", str(error)) + errors["base"] = "cannot_connect" + except Exception as error: # pylint: disable=broad-exception-caught + _LOGGER.error("Unknown exception occurred during validation %s", str(error)) + errors["base"] = "cannot_connect" + + return errors + + class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Train integration.""" @@ -58,15 +117,6 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): entry: config_entries.ConfigEntry | None - async def validate_input( - self, api_key: str, train_from: str, train_to: str - ) -> None: - """Validate input from user input.""" - web_session = async_get_clientsession(self.hass) - train_api = TrafikverketTrain(web_session, api_key) - await train_api.async_get_train_station(train_from) - await train_api.async_get_train_station(train_to) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" @@ -83,19 +133,15 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): api_key = user_input[CONF_API_KEY] assert self.entry is not None - try: - await self.validate_input( - api_key, self.entry.data[CONF_FROM], self.entry.data[CONF_TO] - ) - except InvalidAuthentication: - errors["base"] = "invalid_auth" - except NoTrainStationFound: - errors["base"] = "invalid_station" - except MultipleTrainStationsFound: - errors["base"] = "more_stations" - except Exception: # pylint: disable=broad-exception-caught - errors["base"] = "cannot_connect" - else: + errors = await validate_input( + self.hass, + api_key, + self.entry.data[CONF_FROM], + self.entry.data[CONF_TO], + self.entry.data.get(CONF_TIME), + self.entry.data[CONF_WEEKDAY], + ) + if not errors: self.hass.config_entries.async_update_entry( self.entry, data={ @@ -129,40 +175,36 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if train_time: name = f"{train_from} to {train_to} at {train_time}" - try: - await self.validate_input(api_key, train_from, train_to) - except InvalidAuthentication: - errors["base"] = "invalid_auth" - except NoTrainStationFound: - errors["base"] = "invalid_station" - except MultipleTrainStationsFound: - errors["base"] = "more_stations" - except Exception: # pylint: disable=broad-exception-caught - errors["base"] = "cannot_connect" - else: - if train_time: - if bool(dt_util.parse_time(train_time) is None): - errors["base"] = "invalid_time" - if not errors: - unique_id = create_unique_id( - train_from, train_to, train_time, train_days - ) - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=name, - data={ - CONF_API_KEY: api_key, - CONF_NAME: name, - CONF_FROM: train_from, - CONF_TO: train_to, - CONF_TIME: train_time, - CONF_WEEKDAY: train_days, - }, - ) + errors = await validate_input( + self.hass, + api_key, + train_from, + train_to, + train_time, + train_days, + ) + if not errors: + unique_id = create_unique_id( + train_from, train_to, train_time, train_days + ) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_API_KEY: api_key, + CONF_NAME: name, + CONF_FROM: train_from, + CONF_TO: train_to, + CONF_TIME: train_time, + CONF_WEEKDAY: train_days, + }, + ) return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + DATA_SCHEMA, user_input or {} + ), errors=errors, ) diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py index 3125fea8e39..fac1c418b09 100644 --- a/homeassistant/components/trafikverket_train/coordinator.py +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import date, datetime, time, timedelta +from datetime import datetime, time, timedelta import logging from pytrafikverket import TrafikverketTrain @@ -15,7 +15,7 @@ from pytrafikverket.exceptions import ( from pytrafikverket.trafikverket_train import StationInfo, TrainStop from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_WEEKDAY, WEEKDAYS +from homeassistant.const import CONF_API_KEY, CONF_WEEKDAY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -23,6 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from homeassistant.util import dt as dt_util from .const import CONF_TIME, DOMAIN +from .util import next_departuredate @dataclass @@ -44,27 +45,6 @@ _LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = timedelta(minutes=5) -def _next_weekday(fromdate: date, weekday: int) -> date: - """Return the date of the next time a specific weekday happen.""" - days_ahead = weekday - fromdate.weekday() - if days_ahead <= 0: - days_ahead += 7 - return fromdate + timedelta(days_ahead) - - -def _next_departuredate(departure: list[str]) -> date: - """Calculate the next departuredate from an array input of short days.""" - today_date = date.today() - today_weekday = date.weekday(today_date) - if WEEKDAYS[today_weekday] in departure: - return today_date - for day in departure: - next_departure = WEEKDAYS.index(day) - if next_departure > today_weekday: - return _next_weekday(today_date, next_departure) - return _next_weekday(today_date, WEEKDAYS.index(departure[0])) - - def _get_as_utc(date_value: datetime | None) -> datetime | None: """Return utc datetime or None.""" if date_value: @@ -110,7 +90,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): when = dt_util.now() state: TrainStop | None = None if self._time: - departure_day = _next_departuredate(self._weekdays) + departure_day = next_departuredate(self._weekdays) when = datetime.combine( departure_day, self._time, diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json index 59431107ae2..aabab0907ab 100644 --- a/homeassistant/components/trafikverket_train/strings.json +++ b/homeassistant/components/trafikverket_train/strings.json @@ -9,7 +9,8 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_station": "Could not find a station with the specified name", "more_stations": "Found multiple stations with the specified name", - "invalid_time": "Invalid time provided", + "no_trains": "No train found", + "multiple_trains": "Multiple trains found", "incorrect_api_key": "Invalid API key for selected account" }, "step": { @@ -20,6 +21,9 @@ "from": "From station", "time": "Time (optional)", "weekday": "Days" + }, + "data_description": { + "time": "Set time to search specifically at this time of day, must be exact time as scheduled train departure" } }, "reauth_confirm": { diff --git a/homeassistant/components/trafikverket_train/util.py b/homeassistant/components/trafikverket_train/util.py index 6ed672c9e7e..c5553c4a4a7 100644 --- a/homeassistant/components/trafikverket_train/util.py +++ b/homeassistant/components/trafikverket_train/util.py @@ -1,7 +1,9 @@ """Utils for trafikverket_train.""" from __future__ import annotations -from datetime import time +from datetime import date, time, timedelta + +from homeassistant.const import WEEKDAYS def create_unique_id( @@ -13,3 +15,24 @@ def create_unique_id( f"{from_station.casefold().replace(' ', '')}-{to_station.casefold().replace(' ', '')}" f"-{timestr.casefold().replace(' ', '')}-{str(weekdays)}" ) + + +def next_weekday(fromdate: date, weekday: int) -> date: + """Return the date of the next time a specific weekday happen.""" + days_ahead = weekday - fromdate.weekday() + if days_ahead <= 0: + days_ahead += 7 + return fromdate + timedelta(days_ahead) + + +def next_departuredate(departure: list[str]) -> date: + """Calculate the next departuredate from an array input of short days.""" + today_date = date.today() + today_weekday = date.weekday(today_date) + if WEEKDAYS[today_weekday] in departure: + return today_date + for day in departure: + next_departure = WEEKDAYS.index(day) + if next_departure > today_weekday: + return next_weekday(today_date, next_departure) + return next_weekday(today_date, WEEKDAYS.index(departure[0])) diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 424e1d74162..a3b449755c7 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -6,8 +6,11 @@ from unittest.mock import patch import pytest from pytrafikverket.exceptions import ( InvalidAuthentication, + MultipleTrainAnnouncementFound, MultipleTrainStationsFound, + NoTrainAnnouncementFound, NoTrainStationFound, + UnknownError, ) from homeassistant import config_entries @@ -35,11 +38,13 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_API_KEY: "1234567890", @@ -51,9 +56,9 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Stockholm C to Uppsala C at 10:00" - assert result2["data"] == { + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Stockholm C to Uppsala C at 10:00" + assert result["data"] == { "api_key": "1234567890", "name": "Stockholm C to Uppsala C at 10:00", "from": "Stockholm C", @@ -62,7 +67,7 @@ async def test_form(hass: HomeAssistant) -> None: "weekday": ["mon", "fri"], } assert len(mock_setup_entry.mock_calls) == 1 - assert result2["result"].unique_id == "{}-{}-{}-{}".format( + assert result["result"].unique_id == "{}-{}-{}-{}".format( "stockholmc", "uppsalac", "10:00", "['mon', 'fri']" ) @@ -92,11 +97,13 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_API_KEY: "1234567890", @@ -108,8 +115,8 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "already_configured" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" @pytest.mark.parametrize( @@ -137,19 +144,21 @@ async def test_flow_fails( hass: HomeAssistant, side_effect: Exception, base_error: str ) -> None: """Test config flow errors.""" - result4 = await hass.config_entries.flow.async_init( + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM - assert result4["step_id"] == config_entries.SOURCE_USER + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", side_effect=side_effect(), + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ): - result4 = await hass.config_entries.flow.async_configure( - result4["flow_id"], + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={ CONF_API_KEY: "1234567890", CONF_FROM: "Stockholm C", @@ -157,32 +166,55 @@ async def test_flow_fails( }, ) - assert result4["errors"] == {"base": base_error} + assert result["errors"] == {"base": base_error} -async def test_flow_fails_incorrect_time(hass: HomeAssistant) -> None: - """Test config flow errors due to bad time.""" - result5 = await hass.config_entries.flow.async_init( +@pytest.mark.parametrize( + ("side_effect", "base_error"), + [ + ( + NoTrainAnnouncementFound, + "no_trains", + ), + ( + MultipleTrainAnnouncementFound, + "multiple_trains", + ), + ( + UnknownError, + "cannot_connect", + ), + ], +) +async def test_flow_fails_departures( + hass: HomeAssistant, side_effect: Exception, base_error: str +) -> None: + """Test config flow errors.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result5["type"] == FlowResultType.FORM - assert result5["step_id"] == config_entries.SOURCE_USER + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_next_train_stop", + side_effect=side_effect(), + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ): - result6 = await hass.config_entries.flow.async_configure( - result5["flow_id"], + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={ CONF_API_KEY: "1234567890", CONF_FROM: "Stockholm C", CONF_TO: "Uppsala C", - CONF_TIME: "25:25", }, ) - assert result6["errors"] == {"base": "invalid_time"} + assert result["errors"] == {"base": base_error} async def test_reauth_flow(hass: HomeAssistant) -> None: @@ -216,18 +248,20 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "1234567891"}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", "name": "Stockholm C to Uppsala C at 10:00", @@ -290,31 +324,122 @@ async def test_reauth_flow_error( with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", side_effect=side_effect(), + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "1234567890"}, ) await hass.async_block_till_done() - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": p_error} + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": p_error} with patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "1234567891"}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == { + "api_key": "1234567891", + "name": "Stockholm C to Uppsala C at 10:00", + "from": "Stockholm C", + "to": "Uppsala C", + "time": "10:00", + "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], + } + + +@pytest.mark.parametrize( + ("side_effect", "p_error"), + [ + ( + NoTrainAnnouncementFound, + "no_trains", + ), + ( + MultipleTrainAnnouncementFound, + "multiple_trains", + ), + ( + UnknownError, + "cannot_connect", + ), + ], +) +async def test_reauth_flow_error_departures( + hass: HomeAssistant, side_effect: Exception, p_error: str +) -> None: + """Test a reauthentication flow with error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C at 10:00", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", + ) + 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.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + side_effect=side_effect(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": p_error} + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", "name": "Stockholm C to Uppsala C at 10:00", From 4089bd43da64f094aa0e5ec8ddce9298abc43862 Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:54:06 -0400 Subject: [PATCH 013/179] Fix tomorrowio integration for new users (#97973) The tomorrow.io integration isn't working for new users due to changes made by tomorrow.io. This fixes that with the following changes: * Add 60 minute timestep option * Change default timestep to 60 minutes --- homeassistant/components/tomorrowio/config_flow.py | 2 +- homeassistant/components/tomorrowio/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index cdb0032431c..d6855f42c0a 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -102,7 +102,7 @@ class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): vol.Required( CONF_TIMESTEP, default=self._config_entry.options[CONF_TIMESTEP], - ): vol.In([1, 5, 15, 30]), + ): vol.In([1, 5, 15, 30, 60]), } return self.async_show_form( diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index 51d8d5f31cc..7ad6ea60836 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -25,7 +25,7 @@ LOGGER = logging.getLogger(__package__) CONF_TIMESTEP = "timestep" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] -DEFAULT_TIMESTEP = 15 +DEFAULT_TIMESTEP = 60 DEFAULT_FORECAST_TYPE = DAILY DOMAIN = "tomorrowio" INTEGRATION_NAME = "Tomorrow.io" From a4721e9b365c9387b8a964a8320fccb00ac2f8fc Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 7 Aug 2023 12:07:48 -0400 Subject: [PATCH 014/179] Schlage: Set the changed by attribute on locks based on log messages (#97469) --- .../components/schlage/coordinator.py | 43 ++++++++++++++++--- homeassistant/components/schlage/entity.py | 9 +++- homeassistant/components/schlage/lock.py | 5 +++ tests/components/schlage/conftest.py | 3 ++ tests/components/schlage/test_lock.py | 42 ++++++++++++++++++ 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/schlage/coordinator.py b/homeassistant/components/schlage/coordinator.py index 8b9cde21f90..2b1e8460af2 100644 --- a/homeassistant/components/schlage/coordinator.py +++ b/homeassistant/components/schlage/coordinator.py @@ -1,10 +1,12 @@ """DataUpdateCoordinator for the Schlage integration.""" from __future__ import annotations +import asyncio from dataclasses import dataclass from pyschlage import Lock, Schlage -from pyschlage.exceptions import Error +from pyschlage.exceptions import Error as SchlageError +from pyschlage.log import LockLog from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -12,11 +14,19 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER, UPDATE_INTERVAL +@dataclass +class LockData: + """Container for cached lock data from the Schlage API.""" + + lock: Lock + logs: list[LockLog] + + @dataclass class SchlageData: """Container for cached data from the Schlage API.""" - locks: dict[str, Lock] + locks: dict[str, LockData] class SchlageDataUpdateCoordinator(DataUpdateCoordinator[SchlageData]): @@ -32,10 +42,29 @@ class SchlageDataUpdateCoordinator(DataUpdateCoordinator[SchlageData]): async def _async_update_data(self) -> SchlageData: """Fetch the latest data from the Schlage API.""" try: - return await self.hass.async_add_executor_job(self._update_data) - except Error as ex: + locks = await self.hass.async_add_executor_job(self.api.locks) + except SchlageError as ex: raise UpdateFailed("Failed to refresh Schlage data") from ex + lock_data = await asyncio.gather( + *( + self.hass.async_add_executor_job(self._get_lock_data, lock) + for lock in locks + ) + ) + return SchlageData( + locks={ld.lock.device_id: ld for ld in lock_data}, + ) - def _update_data(self) -> SchlageData: - """Fetch the latest data from the Schlage API.""" - return SchlageData(locks={lock.device_id: lock for lock in self.api.locks()}) + def _get_lock_data(self, lock: Lock) -> LockData: + logs: list[LockLog] = [] + previous_lock_data = None + if self.data and (previous_lock_data := self.data.locks.get(lock.device_id)): + # Default to the previous data, in case a refresh fails. + # It's not critical if we don't have the freshest data. + logs = previous_lock_data.logs + try: + logs = lock.logs() + except SchlageError as ex: + LOGGER.debug('Failed to read logs for lock "%s": %s', lock.name, ex) + + return LockData(lock=lock, logs=logs) diff --git a/homeassistant/components/schlage/entity.py b/homeassistant/components/schlage/entity.py index 3a1a11bc098..ed02269fb32 100644 --- a/homeassistant/components/schlage/entity.py +++ b/homeassistant/components/schlage/entity.py @@ -6,7 +6,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER -from .coordinator import SchlageDataUpdateCoordinator +from .coordinator import LockData, SchlageDataUpdateCoordinator class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]): @@ -29,10 +29,15 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]): sw_version=self._lock.firmware_version, ) + @property + def _lock_data(self) -> LockData: + """Fetch the LockData from our coordinator.""" + return self.coordinator.data.locks[self.device_id] + @property def _lock(self) -> Lock: """Fetch the Schlage lock from our coordinator.""" - return self.coordinator.data.locks[self.device_id] + return self._lock_data.lock @property def available(self) -> bool: diff --git a/homeassistant/components/schlage/lock.py b/homeassistant/components/schlage/lock.py index 65758c3442f..ff9c60c0b55 100644 --- a/homeassistant/components/schlage/lock.py +++ b/homeassistant/components/schlage/lock.py @@ -48,6 +48,11 @@ class SchlageLockEntity(SchlageEntity, LockEntity): """Update our internal state attributes.""" self._attr_is_locked = self._lock.is_locked self._attr_is_jammed = self._lock.is_jammed + # Only update changed_by if we get a valid value. This way a previous + # value will stay intact if the latest log message isn't related to a + # lock state change. + if changed_by := self._lock.last_changed_by(self._lock_data.logs): + self._attr_changed_by = changed_by async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index 3445d653a81..c0be3d28005 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -36,6 +36,7 @@ async def mock_added_config_entry( ) -> MockConfigEntry: """Mock ConfigEntry that's been added to HA.""" mock_schlage.locks.return_value = [mock_lock] + mock_schlage.users.return_value = [] mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -80,4 +81,6 @@ def mock_lock(): battery_level=20, firmware_version="1.0", ) + mock_lock.logs.return_value = [] + mock_lock.last_changed_by.return_value = "thumbturn" return mock_lock diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index b164b4f6b79..bf32d76836c 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -1,11 +1,18 @@ """Test schlage lock.""" + +from datetime import timedelta from unittest.mock import Mock +from pyschlage.exceptions import UnknownError + from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed async def test_lock_device_registry( @@ -43,3 +50,38 @@ async def test_lock_services( mock_lock.unlock.assert_called_once_with() await hass.config_entries.async_unload(mock_added_config_entry.entry_id) + + +async def test_changed_by( + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry +) -> None: + """Test population of the changed_by attribute.""" + mock_lock.last_changed_by.reset_mock() + mock_lock.last_changed_by.return_value = "access code - foo" + + # Make the coordinator refresh data. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + mock_lock.last_changed_by.assert_called_once_with([]) + + lock_device = hass.states.get("lock.vault_door") + assert lock_device is not None + assert lock_device.attributes.get("changed_by") == "access code - foo" + + +async def test_changed_by_uses_previous_logs_on_failure( + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry +) -> None: + """Test that a failure to load logs is not terminal.""" + mock_lock.last_changed_by.reset_mock() + mock_lock.last_changed_by.return_value = "thumbturn" + mock_lock.logs.side_effect = UnknownError("Cannot load logs") + + # Make the coordinator refresh data. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + mock_lock.last_changed_by.assert_called_once_with([]) + + lock_device = hass.states.get("lock.vault_door") + assert lock_device is not None + assert lock_device.attributes.get("changed_by") == "thumbturn" From d56484e2d6cf497d50012d931bd585bd89a069a5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 7 Aug 2023 18:41:08 +0200 Subject: [PATCH 015/179] Fix docstrings in mobile app device tracker (#97963) --- homeassistant/components/mobile_app/device_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index d347a0cc4db..fb555db49cb 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,4 +1,4 @@ -"""Device tracker platform that adds support for OwnTracks over MQTT.""" +"""Device tracker for Mobile app.""" from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, @@ -35,7 +35,7 @@ ATTR_KEYS = (ATTR_ALTITUDE, ATTR_COURSE, ATTR_SPEED, ATTR_VERTICAL_ACCURACY) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up OwnTracks based off an entry.""" + """Set up Mobile app based off an entry.""" entity = MobileAppEntity(entry) async_add_entities([entity]) @@ -44,7 +44,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" def __init__(self, entry, data=None): - """Set up OwnTracks entity.""" + """Set up Mobile app entity.""" self._entry = entry self._data = data self._dispatch_unsub = None From a234ab51fe09b7fc2c02f797bf1737decf8b71fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 06:41:53 -1000 Subject: [PATCH 016/179] Restore bthome state at start when device is in range or sleepy (#97949) --- .../components/bthome/binary_sensor.py | 4 +- homeassistant/components/bthome/sensor.py | 4 +- tests/components/bthome/test_binary_sensor.py | 62 ++++++++++++++++++ tests/components/bthome/test_sensor.py | 64 +++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index 277c2af7ff2..02a226d1f7c 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -186,7 +186,9 @@ async def async_setup_entry( BTHomeBluetoothBinarySensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) class BTHomeBluetoothBinarySensorEntity( diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 95cba20055f..caa652715bf 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -383,7 +383,9 @@ async def async_setup_entry( BTHomeBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class BTHomeBluetoothSensorEntity( diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py index cc5ad13dc80..168988e510f 100644 --- a/tests/components/bthome/test_binary_sensor.py +++ b/tests/components/bthome/test_binary_sensor.py @@ -308,3 +308,65 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sleepy_device_restores_state(hass: HomeAssistant) -> None: + """Test sleepy device does not go to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:8D:18:B2", + data={}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x44\x11\x01", + ), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.test_device_18b2_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.test_device_18b2_opening") + + # Sleepy devices should keep their state over time + assert opening_sensor.state == STATE_ON + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.test_device_18b2_opening") + + # Sleepy devices should keep their state on restore + assert opening_sensor.state == STATE_ON diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 582dcabbb33..7474e3ba890 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1195,3 +1195,67 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.data[CONF_SLEEPY_DEVICE] is True + + +async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: + """Test sleepy device does not go to unavailable after 60 minutes and restores state.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:8D:18:B2", + data={}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x44\x04\x13\x8a\x01", + ), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + pressure_sensor = hass.states.get("sensor.test_device_18b2_pressure") + + assert pressure_sensor.state == "1008.83" + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + pressure_sensor = hass.states.get("sensor.test_device_18b2_pressure") + + # Sleepy devices should keep their state over time + assert pressure_sensor.state == "1008.83" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + pressure_sensor = hass.states.get("sensor.test_device_18b2_pressure") + + # Sleepy devices should keep their state over time and restore it + assert pressure_sensor.state == "1008.83" + + assert entry.data[CONF_SLEEPY_DEVICE] is True From b34ce3c792643dfce7a2d1750d9e99eb922326d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Mon, 7 Aug 2023 19:15:51 +0200 Subject: [PATCH 017/179] Improve airthings ble (#97905) Co-authored-by: J. Nick Koston --- .../components/airthings_ble/config_flow.py | 10 ++++--- .../components/airthings_ble/manifest.json | 2 +- .../components/airthings_ble/sensor.py | 8 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airthings_ble/__init__.py | 3 +++ .../airthings_ble/test_config_flow.py | 26 ++++++++++++++----- 7 files changed, 38 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index 6d5df7ddd56..b562e837ff4 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -34,8 +34,12 @@ class Discovery: def get_name(device: AirthingsDevice) -> str: - """Generate name with identifier for device.""" - return f"{device.name} ({device.identifier})" + """Generate name with model and identifier for device.""" + + name = device.friendly_name() + if identifier := device.identifier: + name += f" ({identifier})" + return name class AirthingsDeviceUpdateError(Exception): @@ -156,7 +160,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="no_devices_found") titles = { - address: get_name(discovery.device) + address: discovery.device.name for (address, discovery) in self._discovered_devices.items() } return self.async_show_form( diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index 8c78bbfb58d..ef9ad3a802e 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "iot_class": "local_polling", - "requirements": ["airthings-ble==0.5.3"] + "requirements": ["airthings-ble==0.5.6-2"] } diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 98190df6b8d..6bcd0337ed1 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -162,10 +162,11 @@ class AirthingsSensor( super().__init__(coordinator) self.entity_description = entity_description - name = f"{airthings_device.name} {airthings_device.identifier}" + name = airthings_device.name + if identifier := airthings_device.identifier: + name += f" ({identifier})" self._attr_unique_id = f"{name}_{entity_description.key}" - self._id = airthings_device.address self._attr_device_info = DeviceInfo( connections={ @@ -175,9 +176,10 @@ class AirthingsSensor( ) }, name=name, - manufacturer="Airthings", + manufacturer=airthings_device.manufacturer, hw_version=airthings_device.hw_version, sw_version=airthings_device.sw_version, + model=airthings_device.model, ) @property diff --git a/requirements_all.txt b/requirements_all.txt index b9933b28105..c683c57b702 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -378,7 +378,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.3 +airthings-ble==0.5.6-2 # homeassistant.components.airthings airthings-cloud==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8dece4fb7e..215ac1389b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -353,7 +353,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.3 +airthings-ble==0.5.6-2 # homeassistant.components.airthings airthings-cloud==0.1.0 diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index 71875b9c4b1..0dd78718a30 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -77,8 +77,11 @@ UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( ) WAVE_DEVICE_INFO = AirthingsDevice( + manufacturer="Airthings AS", hw_version="REV A", sw_version="G-BLE-1.5.3-master+0", + model="Wave Plus", + model_raw="2930", name="Airthings Wave+", identifier="123456", sensors={ diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index 1702140864a..bc009f03027 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -25,7 +25,13 @@ from tests.common import MockConfigEntry async def test_bluetooth_discovery(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device.""" with patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( - AirthingsDevice(name="Airthings Wave+", identifier="123456") + AirthingsDevice( + manufacturer="Airthings AS", + model="Wave Plus", + model_raw="2930", + name="Airthings Wave Plus", + identifier="123456", + ) ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -35,7 +41,9 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" - assert result["description_placeholders"] == {"name": "Airthings Wave+ (123456)"} + assert result["description_placeholders"] == { + "name": "Airthings Wave Plus (123456)" + } with patch_async_setup_entry(): result = await hass.config_entries.flow.async_configure( @@ -43,7 +51,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Airthings Wave+ (123456)" + assert result["title"] == "Airthings Wave Plus (123456)" assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" @@ -100,7 +108,13 @@ async def test_user_setup(hass: HomeAssistant) -> None: "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", return_value=[WAVE_SERVICE_INFO], ), patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( - AirthingsDevice(name="Airthings Wave+", identifier="123456") + AirthingsDevice( + manufacturer="Airthings AS", + model="Wave Plus", + model_raw="2930", + name="Airthings Wave Plus", + identifier="123456", + ) ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -112,7 +126,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: schema = result["data_schema"].schema assert schema.get(CONF_ADDRESS).container == { - "cc:cc:cc:cc:cc:cc": "Airthings Wave+ (123456)" + "cc:cc:cc:cc:cc:cc": "Airthings Wave Plus" } with patch( @@ -125,7 +139,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Airthings Wave+ (123456)" + assert result["title"] == "Airthings Wave Plus (123456)" assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" From fb12c237ab29c213d5c63a6fc2cc2cc9daf8c06b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 07:58:27 -1000 Subject: [PATCH 018/179] Restore xiaomi_ble state at start when device is in range or sleepy (#97979) --- .../components/xiaomi_ble/binary_sensor.py | 4 +- homeassistant/components/xiaomi_ble/sensor.py | 4 +- .../xiaomi_ble/test_binary_sensor.py | 61 +++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 65 ++++++++++++++++++- 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 5490676ad1a..2894b8d2f3f 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -122,7 +122,9 @@ async def async_setup_entry( XiaomiBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) class XiaomiBluetoothSensorEntity( diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 56bfbb1b020..cdb7b3a8fd8 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -195,7 +195,9 @@ async def async_setup_entry( XiaomiBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class XiaomiBluetoothSensorEntity( diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 5dd1b965f25..32d1fea7f62 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -367,3 +367,64 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.data[CONF_SLEEPY_DEVICE] is True + + +async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: + """Test sleepy device does not go to unavailable after 60 minutes and restores state.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"@0\xd6\x03$\x19\x10\x01\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Sleepy devices should keep their state over time + assert opening_sensor.state == STATE_ON + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Sleepy devices should keep their state over time and restore it + assert opening_sensor.state == STATE_ON + + assert entry.data[CONF_SLEEPY_DEVICE] is True diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index a2b0e62821a..b0ddd99a7c2 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.components.xiaomi_ble.const import CONF_SLEEPY_DEVICE, DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -713,7 +713,7 @@ async def test_unavailable(hass: HomeAssistant) -> None: async def test_sleepy_device(hass: HomeAssistant) -> None: - """Test normal device goes to unavailable after 60 minutes.""" + """Test sleepy devices stay available.""" start_monotonic = time.monotonic() entry = MockConfigEntry( @@ -759,3 +759,64 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: + """Test sleepy devices stay available.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="50:FB:19:1B:B5:DC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + assert mass_non_stabilized_sensor.state == "86.55" + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + + # Sleepy devices should keep their state over time + assert mass_non_stabilized_sensor.state == "86.55" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + + # Sleepy devices should keep their state over time and restore it + assert mass_non_stabilized_sensor.state == "86.55" + + assert entry.data[CONF_SLEEPY_DEVICE] is True From 40a221c9239a63b1f52b016d2dd8d40bfb329294 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Aug 2023 20:36:30 +0200 Subject: [PATCH 019/179] Alexa typing part 1 (#97909) * Typing part 1 * mypy * Correct typing for logbook --- homeassistant/components/alexa/auth.py | 46 +++++++++++-------- homeassistant/components/alexa/config.py | 46 ++++++++++--------- homeassistant/components/alexa/const.py | 2 +- homeassistant/components/alexa/errors.py | 13 ++++-- .../components/alexa/flash_briefings.py | 13 ++++-- homeassistant/components/alexa/logbook.py | 12 +++-- .../components/alexa/state_report.py | 2 + 7 files changed, 83 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 86c038e2da8..ea237e4c92c 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,15 +1,16 @@ """Support for Alexa skill auth.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta from http import HTTPStatus import json import logging +from typing import Any import aiohttp import async_timeout from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util @@ -30,24 +31,24 @@ STORAGE_REFRESH_TOKEN = "refresh_token" class Auth: """Handle authentication to send events to Alexa.""" - def __init__(self, hass, client_id, client_secret): + def __init__(self, hass: HomeAssistant, client_id: str, client_secret: str) -> None: """Initialize the Auth class.""" self.hass = hass self.client_id = client_id self.client_secret = client_secret - self._prefs = None - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._prefs: dict[str, Any] | None = None + self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._get_token_lock = asyncio.Lock() - async def async_do_auth(self, accept_grant_code): + async def async_do_auth(self, accept_grant_code: str) -> str | None: """Do authentication with an AcceptGrant code.""" # access token not retrieved yet for the first time, so this should # be an access token request - lwa_params = { + lwa_params: dict[str, str] = { "grant_type": "authorization_code", "code": accept_grant_code, CONF_CLIENT_ID: self.client_id, @@ -61,16 +62,18 @@ class Auth: return await self._async_request_new_token(lwa_params) @callback - def async_invalidate_access_token(self): + def async_invalidate_access_token(self) -> None: """Invalidate access token.""" + assert self._prefs is not None self._prefs[STORAGE_ACCESS_TOKEN] = None - async def async_get_access_token(self): + async def async_get_access_token(self) -> str | None: """Perform access token or token refresh request.""" async with self._get_token_lock: if self._prefs is None: await self.async_load_preferences() + assert self._prefs is not None if self.is_token_valid(): _LOGGER.debug("Token still valid, using it") return self._prefs[STORAGE_ACCESS_TOKEN] @@ -79,7 +82,7 @@ class Auth: _LOGGER.debug("Token invalid and no refresh token available") return None - lwa_params = { + lwa_params: dict[str, str] = { "grant_type": "refresh_token", "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN], CONF_CLIENT_ID: self.client_id, @@ -90,19 +93,23 @@ class Auth: return await self._async_request_new_token(lwa_params) @callback - def is_token_valid(self): + def is_token_valid(self) -> bool: """Check if a token is already loaded and if it is still valid.""" + assert self._prefs is not None if not self._prefs[STORAGE_ACCESS_TOKEN]: return False - expire_time = dt_util.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME]) + expire_time: datetime | None = dt_util.parse_datetime( + self._prefs[STORAGE_EXPIRE_TIME] + ) + assert expire_time is not None preemptive_expire_time = expire_time - timedelta( seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS ) return dt_util.utcnow() < preemptive_expire_time - async def _async_request_new_token(self, lwa_params): + async def _async_request_new_token(self, lwa_params: dict[str, str]) -> str | None: try: session = aiohttp_client.async_get_clientsession(self.hass) async with async_timeout.timeout(10): @@ -127,9 +134,9 @@ class Auth: response_json = await response.json() _LOGGER.debug("LWA response body : %s", response_json) - access_token = response_json["access_token"] - refresh_token = response_json["refresh_token"] - expires_in = response_json["expires_in"] + access_token: str = response_json["access_token"] + refresh_token: str = response_json["refresh_token"] + expires_in: int = response_json["expires_in"] expire_time = dt_util.utcnow() + timedelta(seconds=expires_in) await self._async_update_preferences( @@ -138,7 +145,7 @@ class Auth: return access_token - async def async_load_preferences(self): + async def async_load_preferences(self) -> None: """Load preferences with stored tokens.""" self._prefs = await self._store.async_load() @@ -149,10 +156,13 @@ class Auth: STORAGE_EXPIRE_TIME: None, } - async def _async_update_preferences(self, access_token, refresh_token, expire_time): + async def _async_update_preferences( + self, access_token: str, refresh_token: str, expire_time: str + ) -> None: """Update user preferences.""" if self._prefs is None: await self.async_load_preferences() + assert self._prefs is not None if access_token is not None: self._prefs[STORAGE_ACCESS_TOKEN] = access_token diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index d47a548979e..8c9965662bc 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -4,6 +4,9 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio import logging +from typing import Any + +from yarl import URL from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.storage import Store @@ -33,38 +36,38 @@ class AbstractConfig(ABC): await self._store.async_load() @property - def supports_auth(self): + def supports_auth(self) -> bool: """Return if config supports auth.""" return False @property - def should_report_state(self): + def should_report_state(self) -> bool: """Return if states should be proactively reported.""" return False @property - def endpoint(self): + @abstractmethod + def endpoint(self) -> str | URL | None: """Endpoint for report state.""" - return None @property @abstractmethod - def locale(self): + def locale(self) -> str | None: """Return config locale.""" @property - def entity_config(self): + def entity_config(self) -> dict[str, Any]: """Return entity config.""" return {} @property - def is_reporting_states(self): + def is_reporting_states(self) -> bool: """Return if proactive mode is enabled.""" return self._unsub_proactive_report is not None @callback @abstractmethod - def user_identifier(self): + def user_identifier(self) -> str: """Return an identifier for the user that represents this config.""" async def async_enable_proactive_mode(self) -> None: @@ -85,29 +88,29 @@ class AbstractConfig(ABC): self._unsub_proactive_report = None @callback - def should_expose(self, entity_id): + def should_expose(self, entity_id: str) -> bool: """If an entity should be exposed.""" return False @callback - def async_invalidate_access_token(self): + def async_invalidate_access_token(self) -> None: """Invalidate access token.""" raise NotImplementedError - async def async_get_access_token(self): + async def async_get_access_token(self) -> str | None: """Get an access token.""" raise NotImplementedError - async def async_accept_grant(self, code): + async def async_accept_grant(self, code: str) -> str | None: """Accept a grant.""" raise NotImplementedError @property - def authorized(self): + def authorized(self) -> bool: """Return authorization status.""" return self._store.authorized - async def set_authorized(self, authorized) -> None: + async def set_authorized(self, authorized: bool) -> None: """Set authorization status. - Set when an incoming message is received from Alexa. @@ -132,25 +135,26 @@ class AlexaConfigStore: _STORAGE_VERSION = 1 _STORAGE_KEY = DOMAIN - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize a configuration store.""" - self._data = None + self._data: dict[str, Any] | None = None self._hass = hass - self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) + self._store: Store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) @property - def authorized(self): + def authorized(self) -> bool: """Return authorization status.""" + assert self._data is not None return self._data[STORE_AUTHORIZED] @callback - def set_authorized(self, authorized): + def set_authorized(self, authorized: bool) -> None: """Set authorization status.""" - if authorized != self._data[STORE_AUTHORIZED]: + if self._data is not None and authorized != self._data[STORE_AUTHORIZED]: self._data[STORE_AUTHORIZED] = authorized self._store.async_delay_save(lambda: self._data, 1.0) - async def async_load(self): + async def async_load(self) -> None: """Load saved configuration from disk.""" if data := await self._store.async_load(): self._data = data diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 9e1c9e589c1..f71bc091106 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -69,7 +69,7 @@ API_TEMP_UNITS = { # Needs to be ordered dict for `async_api_set_thermostat_mode` which does a # reverse mapping of this dict and we want to map the first occurrence of OFF # back to HA state. -API_THERMOSTAT_MODES = OrderedDict( +API_THERMOSTAT_MODES: OrderedDict[str, str] = OrderedDict( [ (climate.HVACMode.HEAT, "HEAT"), (climate.HVACMode.COOL, "COOL"), diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 7f4b41b9ec7..2c5ced62403 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -1,8 +1,9 @@ """Alexa related errors.""" from __future__ import annotations -from typing import Literal +from typing import Any, Literal +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from .const import API_TEMP_UNITS @@ -29,7 +30,9 @@ class AlexaError(Exception): namespace: str | None = None error_type: str | None = None - def __init__(self, error_message, payload=None): + def __init__( + self, error_message: str, payload: dict[str, Any] | None = None + ) -> None: """Initialize an alexa error.""" Exception.__init__(self) self.error_message = error_message @@ -42,7 +45,7 @@ class AlexaInvalidEndpointError(AlexaError): namespace = "Alexa" error_type = "NO_SUCH_ENDPOINT" - def __init__(self, endpoint_id): + def __init__(self, endpoint_id: str) -> None: """Initialize invalid endpoint error.""" msg = f"The endpoint {endpoint_id} does not exist" AlexaError.__init__(self, msg) @@ -93,7 +96,9 @@ class AlexaTempRangeError(AlexaError): namespace = "Alexa" error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE" - def __init__(self, hass, temp, min_temp, max_temp): + def __init__( + self, hass: HomeAssistant, temp: float, min_temp: float, max_temp: float + ) -> None: """Initialize TempRange error.""" unit = hass.config.units.temperature_unit temp_range = { diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 6f53d86d444..3361908ce9a 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -4,10 +4,13 @@ from http import HTTPStatus import logging import uuid +from aiohttp.web_response import StreamResponse + from homeassistant.components import http from homeassistant.const import CONF_PASSWORD -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import ( @@ -32,7 +35,7 @@ FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}" @callback -def async_setup(hass, flash_briefing_config): +def async_setup(hass: HomeAssistant, flash_briefing_config: ConfigType) -> None: """Activate Alexa component.""" hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config)) @@ -44,14 +47,16 @@ class AlexaFlashBriefingView(http.HomeAssistantView): requires_auth = False name = "api:alexa:flash_briefings" - def __init__(self, hass, flash_briefings): + def __init__(self, hass: HomeAssistant, flash_briefings: ConfigType) -> None: """Initialize Alexa view.""" super().__init__() self.flash_briefings = flash_briefings template.attach(hass, self.flash_briefings) @callback - def get(self, request, briefing_id): + def get( + self, request: http.HomeAssistantRequest, briefing_id: str + ) -> StreamResponse | tuple[bytes, HTTPStatus]: """Handle Alexa Flash Briefing request.""" _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index 496989c57de..cb6835c7ba5 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -1,20 +1,26 @@ """Describe logbook events.""" +from collections.abc import Callable +from typing import Any + from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, ) -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, callback from .const import DOMAIN, EVENT_ALEXA_SMART_HOME @callback -def async_describe_events(hass, async_describe_event): +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: """Describe logbook events.""" @callback - def async_describe_logbook_event(event): + def async_describe_logbook_event(event: Event) -> dict[str, Any]: """Describe a logbook event.""" data = event.data diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 808e0eac482..ecec1451497 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -416,6 +416,7 @@ async def async_send_add_or_update_message( message_serialized = message.serialize() session = async_get_clientsession(hass) + assert config.endpoint is not None return await session.post( config.endpoint, headers=headers, json=message_serialized, allow_redirects=True ) @@ -451,6 +452,7 @@ async def async_send_delete_message( message_serialized = message.serialize() session = async_get_clientsession(hass) + assert config.endpoint is not None return await session.post( config.endpoint, headers=headers, json=message_serialized, allow_redirects=True ) From b7f936fcdda3cff94502d3cc464e67e8555e43b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 09:13:50 -1000 Subject: [PATCH 020/179] Restore govee_ble state when gateway device becomes available (#97984) --- homeassistant/components/govee_ble/sensor.py | 4 +- tests/components/govee_ble/__init__.py | 25 +++++ tests/components/govee_ble/test_sensor.py | 99 +++++++++++++++++++- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index b2da37bdf7e..cbef769bdc9 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -110,7 +110,9 @@ async def async_setup_entry( GoveeBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class GoveeBluetoothSensorEntity( diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 54e7c1ee777..5dd67adb160 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -37,6 +37,31 @@ GVH5177_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +GVH5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( + name="B51782BC8", + address="A4:C1:38:75:2B:C8", + rssi=-66, + manufacturer_data={ + 1: b"\x01\x01\x01\x00\x2a\xf7\x64\x00\x03", + 76: b"\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2", + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) +GVH5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( + name="B51782BC8", + address="A4:C1:38:75:2B:C8", + rssi=-66, + manufacturer_data={ + 1: b"\x01\x01\x00\x00\x2a\xf7\x64\x00\x03", + 76: b"\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2", + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) + GVH5178_SERVICE_INFO_ERROR = BluetoothServiceInfo( name="B51782BC8", address="A4:C1:38:75:2B:C8", diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 1408a35142a..185ae2404da 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -1,4 +1,11 @@ """Test the Govee BLE sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) from homeassistant.components.govee_ble.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( @@ -7,11 +14,20 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from . import GVH5075_SERVICE_INFO, GVH5178_SERVICE_INFO_ERROR +from . import ( + GVH5075_SERVICE_INFO, + GVH5178_PRIMARY_SERVICE_INFO, + GVH5178_REMOTE_SERVICE_INFO, + GVH5178_SERVICE_INFO_ERROR, +) -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) async def test_sensors(hass: HomeAssistant) -> None: @@ -62,3 +78,80 @@ async def test_gvh5178_error(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_gvh5178_multi_sensor(hass: HomeAssistant) -> None: + """Test H5178 with a primary and remote sensor. + + The gateway sensor is responsible for broadcasting the state for + all sensors and it does so in many advertisements. We want + all the connected devices to stay available when the gateway + sensor is available. + """ + start_monotonic = time.monotonic() + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:75:2B:C8", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info(hass, GVH5178_REMOTE_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.b51782bc8_remote_temperature") + assert temp_sensor.state == "1.0" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + temp_sensor = hass.states.get("sensor.b51782bc8_remote_temperature") + assert temp_sensor.state == STATE_UNAVAILABLE + + inject_bluetooth_service_info(hass, GVH5178_PRIMARY_SERVICE_INFO) + await hass.async_block_till_done() + + temp_sensor = hass.states.get("sensor.b51782bc8_remote_temperature") + assert temp_sensor.state == "1.0" + + primary_temp_sensor = hass.states.get("sensor.b51782bc8_primary_temperature") + assert primary_temp_sensor.state == "1.0" + + # Fastforward time without BLE advertisements + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + temp_sensor = hass.states.get("sensor.b51782bc8_remote_temperature") + assert temp_sensor.state == STATE_UNAVAILABLE + + primary_temp_sensor = hass.states.get("sensor.b51782bc8_primary_temperature") + assert primary_temp_sensor.state == STATE_UNAVAILABLE From 56c2276630647cbca4caa7ab518052fe6479a4a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 09:19:46 -1000 Subject: [PATCH 021/179] Restore sleepy oralb devices state at startup (#97983) --- homeassistant/components/oralb/sensor.py | 4 ++- tests/components/oralb/test_sensor.py | 35 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 76104c75164..16118361ab8 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -111,7 +111,9 @@ async def async_setup_entry( OralBBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class OralBBluetoothSensorEntity( diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index d43997fe7ed..2abc27c8b14 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -9,7 +9,10 @@ from homeassistant.components.bluetooth import ( async_address_present, ) from homeassistant.components.oralb.const import DOMAIN -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_FRIENDLY_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -31,6 +34,7 @@ async def test_sensors( hass: HomeAssistant, entity_registry_enabled_by_default: None ) -> None: """Test setting up creates the sensors.""" + start_monotonic = time.monotonic() entry = MockConfigEntry( domain=DOMAIN, unique_id=ORALB_SERVICE_INFO.address, @@ -59,6 +63,30 @@ async def test_sensors( assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # All of these devices are sleepy so we should still be available + toothbrush_sensor = hass.states.get( + "sensor.smart_series_7000_48be_toothbrush_state" + ) + toothbrush_sensor_attrs = toothbrush_sensor.attributes + assert toothbrush_sensor.state == "running" + async def test_sensors_io_series_4( hass: HomeAssistant, entity_registry_enabled_by_default: None @@ -103,6 +131,11 @@ async def test_sensors_io_series_4( async_address_present(hass, ORALB_IO_SERIES_4_SERVICE_INFO.address) is False ) + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_mode") # Sleepy devices should keep their state over time assert toothbrush_sensor.state == "gum care" From d304d420514eae3f62854eccc02a10f0333472bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 09:27:18 -1000 Subject: [PATCH 022/179] Restore qingping state when device becomes available (#97980) --- .../components/qingping/binary_sensor.py | 4 +- homeassistant/components/qingping/sensor.py | 4 +- .../components/qingping/test_binary_sensor.py | 78 +++++++++++++++++- tests/components/qingping/test_sensor.py | 80 ++++++++++++++++++- 4 files changed, 156 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index 3a40e1baa09..99bcf83ec1a 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -87,7 +87,9 @@ async def async_setup_entry( QingpingBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) class QingpingBluetoothSensorEntity( diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index 4ee1db90c6f..bc99ed80ff3 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -155,7 +155,9 @@ async def async_setup_entry( QingpingBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class QingpingBluetoothSensorEntity( diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py index 5733f4f145b..78752372baa 100644 --- a/tests/components/qingping/test_binary_sensor.py +++ b/tests/components/qingping/test_binary_sensor.py @@ -1,12 +1,27 @@ """Test the Qingping binary sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) from homeassistant.components.qingping.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from . import LIGHT_AND_SIGNAL_SERVICE_INFO +from . import LIGHT_AND_SIGNAL_SERVICE_INFO, NO_DATA_SERVICE_INFO -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) async def test_binary_sensors(hass: HomeAssistant) -> None: @@ -31,3 +46,58 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: + """Test setting up creates the binary sensors and restoring state.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("binary_sensor")) == 0 + inject_bluetooth_service_info(hass, LIGHT_AND_SIGNAL_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("binary_sensor")) == 1 + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == STATE_OFF + assert motion_sensor.attributes[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Motion" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Device is no longer available because its not in range + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == STATE_UNAVAILABLE + + # Device is back in range + + inject_bluetooth_service_info(hass, NO_DATA_SERVICE_INFO) + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == STATE_OFF diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py index d80522f47c9..2fedbba9e5c 100644 --- a/tests/components/qingping/test_sensor.py +++ b/tests/components/qingping/test_sensor.py @@ -1,13 +1,28 @@ """Test the Qingping sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) from homeassistant.components.qingping.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from . import LIGHT_AND_SIGNAL_SERVICE_INFO +from . import LIGHT_AND_SIGNAL_SERVICE_INFO, NO_DATA_SERVICE_INFO -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) async def test_sensors(hass: HomeAssistant) -> None: @@ -35,3 +50,60 @@ async def test_sensors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: + """Test setting up creates the binary sensors and restoring state.""" + start_monotonic = time.monotonic() + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, LIGHT_AND_SIGNAL_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + lux_sensor_attrs = lux_sensor.attributes + assert lux_sensor.state == "13" + assert lux_sensor_attrs[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Illuminance" + assert lux_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert lux_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Device is no longer available because its not in range + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + assert lux_sensor.state == STATE_UNAVAILABLE + + # Device is back in range + + inject_bluetooth_service_info(hass, NO_DATA_SERVICE_INFO) + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + assert lux_sensor.state == "13" From 7080e0c280282ad23d64580ff40466ddb0ff5e8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 11:00:30 -1000 Subject: [PATCH 023/179] Bump yalexs to 1.5.2 (#97991) --- 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 0dbc4c8f7d6..98c9cbacbda 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.3"] + "requirements": ["yalexs==1.5.2", "yalexs-ble==2.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c683c57b702..261a3f9a707 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2725,7 +2725,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.2.3 # homeassistant.components.august -yalexs==1.5.1 +yalexs==1.5.2 # homeassistant.components.yeelight yeelight==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 215ac1389b4..f6af5170a7b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2007,7 +2007,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.2.3 # homeassistant.components.august -yalexs==1.5.1 +yalexs==1.5.2 # homeassistant.components.yeelight yeelight==0.7.13 From c8256d1d3d26a2d0d5fa318d0599e1d8c8db051f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 11:09:32 -1000 Subject: [PATCH 024/179] Optimize august timings to prepare for Yale Doorman support (#97940) --- homeassistant/components/august/__init__.py | 12 +- homeassistant/components/august/activity.py | 151 +++++++++++------- homeassistant/components/august/subscriber.py | 34 ++-- tests/components/august/conftest.py | 13 ++ tests/components/august/mocks.py | 35 ++-- tests/components/august/test_init.py | 17 +- 6 files changed, 159 insertions(+), 103 deletions(-) create mode 100644 tests/components/august/conftest.py diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 8738b58dab9..408d6e0be7e 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -3,8 +3,10 @@ from __future__ import annotations import asyncio from collections.abc import ValuesView +from datetime import datetime from itertools import chain import logging +from typing import Any from aiohttp import ClientError, ClientResponseError from yalexs.const import DEFAULT_BRAND @@ -238,14 +240,18 @@ class AugustData(AugustSubscriberMixin): ) @callback - def async_pubnub_message(self, device_id, date_time, message): + def async_pubnub_message( + self, device_id: str, date_time: datetime, message: dict[str, Any] + ) -> None: """Process a pubnub message.""" device = self.get_device_detail(device_id) activities = activities_from_pubnub_message(device, date_time, message) + activity_stream = self.activity_stream + assert activity_stream is not None if activities: - self.activity_stream.async_process_newer_device_activities(activities) + activity_stream.async_process_newer_device_activities(activities) self.async_signal_device_id_update(device.device_id) - self.activity_stream.async_schedule_house_id_refresh(device.house_id) + activity_stream.async_schedule_house_id_refresh(device.house_id) @callback def async_stop(self): diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index ad9045a3d0d..3909e36ded8 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -1,16 +1,24 @@ """Consume the august activity stream.""" import asyncio +from datetime import datetime import logging from aiohttp import ClientError +from yalexs.activity import ( + Activity, + ActivityType, +) +from yalexs.api_async import ApiAsync +from yalexs.pubnub_async import AugustPubNub from yalexs.util import get_latest_activity -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow from .const import ACTIVITY_UPDATE_INTERVAL +from .gateway import AugustGateway from .subscriber import AugustSubscriberMixin _LOGGER = logging.getLogger(__name__) @@ -18,29 +26,50 @@ _LOGGER = logging.getLogger(__name__) ACTIVITY_STREAM_FETCH_LIMIT = 10 ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 +# If there is a storm of activity (ie lock, unlock, door open, door close, etc) +# we want to debounce the updates so we don't hammer the activity api too much. +ACTIVITY_DEBOUNCE_COOLDOWN = 3 + + +@callback +def _async_cancel_future_scheduled_updates(cancels: list[CALLBACK_TYPE]) -> None: + """Cancel future scheduled updates.""" + for cancel in cancels: + cancel() + cancels.clear() + class ActivityStream(AugustSubscriberMixin): """August activity stream handler.""" - def __init__(self, hass, api, august_gateway, house_ids, pubnub): + def __init__( + self, + hass: HomeAssistant, + api: ApiAsync, + august_gateway: AugustGateway, + house_ids: set[str], + pubnub: AugustPubNub, + ) -> None: """Init August activity stream object.""" super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) self._hass = hass - self._schedule_updates = {} + self._schedule_updates: dict[str, list[CALLBACK_TYPE]] = {} self._august_gateway = august_gateway self._api = api self._house_ids = house_ids - self._latest_activities = {} - self._last_update_time = None + self._latest_activities: dict[str, dict[ActivityType, Activity]] = {} + self._did_first_update = False self.pubnub = pubnub - self._update_debounce = {} + self._update_debounce: dict[str, Debouncer] = {} async def async_setup(self): """Token refresh check and catch up the activity stream.""" - for house_id in self._house_ids: - self._update_debounce[house_id] = self._async_create_debouncer(house_id) - + self._update_debounce = { + house_id: self._async_create_debouncer(house_id) + for house_id in self._house_ids + } await self._async_refresh(utcnow()) + self._did_first_update = True @callback def _async_create_debouncer(self, house_id): @@ -52,7 +81,7 @@ class ActivityStream(AugustSubscriberMixin): return Debouncer( self._hass, _LOGGER, - cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(), + cooldown=ACTIVITY_DEBOUNCE_COOLDOWN, immediate=True, function=_async_update_house_id, ) @@ -62,73 +91,73 @@ class ActivityStream(AugustSubscriberMixin): """Cleanup any debounces.""" for debouncer in self._update_debounce.values(): debouncer.async_cancel() - for house_id, updater in self._schedule_updates.items(): - if updater is not None: - updater() - self._schedule_updates[house_id] = None + for cancels in self._schedule_updates.values(): + _async_cancel_future_scheduled_updates(cancels) - def get_latest_device_activity(self, device_id, activity_types): + def get_latest_device_activity( + self, device_id: str, activity_types: set[ActivityType] + ) -> Activity | None: """Return latest activity that is one of the activity_types.""" - if device_id not in self._latest_activities: + if not (latest_device_activities := self._latest_activities.get(device_id)): return None - latest_device_activities = self._latest_activities[device_id] - latest_activity = None + latest_activity: Activity | None = None for activity_type in activity_types: - if activity_type in latest_device_activities: + if activity := latest_device_activities.get(activity_type): if ( - latest_activity is not None - and latest_device_activities[activity_type].activity_start_time + latest_activity + and activity.activity_start_time <= latest_activity.activity_start_time ): continue - latest_activity = latest_device_activities[activity_type] + latest_activity = activity return latest_activity - async def _async_refresh(self, time): + async def _async_refresh(self, time: datetime) -> None: """Update the activity stream from August.""" # This is the only place we refresh the api token await self._august_gateway.async_refresh_access_token_if_needed() if self.pubnub.connected: _LOGGER.debug("Skipping update because pubnub is connected") return - await self._async_update_device_activities(time) - - 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 - ) + *(debouncer.async_call() for debouncer in self._update_debounce.values()) ) - self._last_update_time = time @callback - def async_schedule_house_id_refresh(self, house_id): + def async_schedule_house_id_refresh(self, house_id: str) -> None: """Update for a house activities now and once in the future.""" - if self._schedule_updates.get(house_id): - self._schedule_updates[house_id]() - self._schedule_updates[house_id] = None + if cancels := self._schedule_updates.get(house_id): + _async_cancel_future_scheduled_updates(cancels) - async def _update_house_activities(_): - await self._update_debounce[house_id].async_call() + debouncer = self._update_debounce[house_id] - self._hass.async_create_task(self._update_debounce[house_id].async_call()) - # Schedule an update past the debounce to ensure - # we catch the case where the lock operator is - # not updated or the lock failed - self._schedule_updates[house_id] = async_call_later( - self._hass, - ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1, - _update_house_activities, - ) + self._hass.async_create_task(debouncer.async_call()) + # Schedule two updates past the debounce time + # to ensure we catch the case where the activity + # api does not update right away and we need to poll + # it again. Sometimes the lock operator or a doorbell + # will not show up in the activity stream right away. + future_updates = self._schedule_updates.setdefault(house_id, []) - async def _async_update_house_id(self, house_id): + async def _update_house_activities(now: datetime) -> None: + await debouncer.async_call() + + for step in (1, 2): + future_updates.append( + async_call_later( + self._hass, + (step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1, + _update_house_activities, + ) + ) + + async def _async_update_house_id(self, house_id: str) -> None: """Update device activities for a house.""" - if self._last_update_time: + if self._did_first_update: limit = ACTIVITY_STREAM_FETCH_LIMIT else: limit = ACTIVITY_CATCH_UP_FETCH_LIMIT @@ -150,36 +179,34 @@ class ActivityStream(AugustSubscriberMixin): _LOGGER.debug( "Completed retrieving device activities for house id %s", house_id ) - - updated_device_ids = self.async_process_newer_device_activities(activities) - - if not updated_device_ids: - return - - for device_id in updated_device_ids: + for device_id in self.async_process_newer_device_activities(activities): _LOGGER.debug( "async_signal_device_id_update (from activity stream): %s", device_id, ) self.async_signal_device_id_update(device_id) - def async_process_newer_device_activities(self, activities): + def async_process_newer_device_activities( + self, activities: list[Activity] + ) -> set[str]: """Process activities if they are newer than the last one.""" updated_device_ids = set() + latest_activities = self._latest_activities for activity in activities: device_id = activity.device_id activity_type = activity.activity_type - device_activities = self._latest_activities.setdefault(device_id, {}) + device_activities = latest_activities.setdefault(device_id, {}) # Ignore activities that are older than the latest one unless it is a non # locking or unlocking activity with the exact same start time. - if ( - get_latest_activity(activity, device_activities.get(activity_type)) - != activity - ): + last_activity = device_activities.get(activity_type) + # The activity stream can have duplicate activities. So we need + # to call get_latest_activity to figure out if if the activity + # is actually newer than the last one. + latest_activity = get_latest_activity(activity, last_activity) + if latest_activity != activity: continue device_activities[activity_type] = activity - updated_device_ids.add(device_id) return updated_device_ids diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 62aef44a9ee..138887ed09e 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -1,25 +1,30 @@ """Base class for August entity.""" +from abc import abstractmethod +from datetime import datetime, timedelta + from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval class AugustSubscriberMixin: """Base implementation for a subscriber.""" - def __init__(self, hass, update_interval): + def __init__(self, hass: HomeAssistant, update_interval: timedelta) -> None: """Initialize an subscriber.""" super().__init__() self._hass = hass self._update_interval = update_interval - self._subscriptions = {} - self._unsub_interval = None - self._stop_interval = None + self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {} + self._unsub_interval: CALLBACK_TYPE | None = None + self._stop_interval: CALLBACK_TYPE | None = None @callback - def async_subscribe_device_id(self, device_id, update_callback): + def async_subscribe_device_id( + self, device_id: str, update_callback: CALLBACK_TYPE + ) -> CALLBACK_TYPE: """Add an callback subscriber. Returns a callable that can be used to unsubscribe. @@ -34,8 +39,12 @@ class AugustSubscriberMixin: return _unsubscribe + @abstractmethod + async def _async_refresh(self, time: datetime) -> None: + """Refresh data.""" + @callback - def _async_setup_listeners(self): + def _async_setup_listeners(self) -> None: """Create interval and stop listeners.""" self._unsub_interval = async_track_time_interval( self._hass, @@ -54,7 +63,9 @@ class AugustSubscriberMixin: ) @callback - def async_unsubscribe_device_id(self, device_id, update_callback): + def async_unsubscribe_device_id( + self, device_id: str, update_callback: CALLBACK_TYPE + ) -> None: """Remove a callback subscriber.""" self._subscriptions[device_id].remove(update_callback) if not self._subscriptions[device_id]: @@ -63,14 +74,15 @@ class AugustSubscriberMixin: if self._subscriptions: return - self._unsub_interval() - self._unsub_interval = None + if self._unsub_interval: + self._unsub_interval() + self._unsub_interval = None if self._stop_interval: self._stop_interval() self._stop_interval = None @callback - def async_signal_device_id_update(self, device_id): + def async_signal_device_id_update(self, device_id: str) -> None: """Call the callbacks for a device_id.""" if not self._subscriptions.get(device_id): return diff --git a/tests/components/august/conftest.py b/tests/components/august/conftest.py new file mode 100644 index 00000000000..1cb52966fea --- /dev/null +++ b/tests/components/august/conftest.py @@ -0,0 +1,13 @@ +"""August tests conftest.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(name="mock_discovery", autouse=True) +def mock_discovery_fixture(): + """Mock discovery to avoid loading the whole bluetooth stack.""" + with patch( + "homeassistant.components.august.discovery_flow.async_create_flow" + ) as mock_discovery: + yield mock_discovery diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index d5517f64249..910c1d29ed6 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -162,24 +162,23 @@ async def _create_august_api_with_devices( # noqa: C901 _mock_door_operation_activity(lock, "dooropen", 0), ] - if "get_lock_detail" not in api_call_side_effects: - api_call_side_effects["get_lock_detail"] = get_lock_detail_side_effect - if "get_doorbell_detail" not in api_call_side_effects: - api_call_side_effects["get_doorbell_detail"] = get_doorbell_detail_side_effect - if "get_operable_locks" not in api_call_side_effects: - api_call_side_effects["get_operable_locks"] = get_operable_locks_side_effect - if "get_doorbells" not in api_call_side_effects: - api_call_side_effects["get_doorbells"] = get_doorbells_side_effect - if "get_house_activities" not in api_call_side_effects: - api_call_side_effects["get_house_activities"] = get_house_activities_side_effect - if "lock_return_activities" not in api_call_side_effects: - api_call_side_effects[ - "lock_return_activities" - ] = lock_return_activities_side_effect - if "unlock_return_activities" not in api_call_side_effects: - api_call_side_effects[ - "unlock_return_activities" - ] = unlock_return_activities_side_effect + api_call_side_effects.setdefault("get_lock_detail", get_lock_detail_side_effect) + api_call_side_effects.setdefault( + "get_doorbell_detail", get_doorbell_detail_side_effect + ) + api_call_side_effects.setdefault( + "get_operable_locks", get_operable_locks_side_effect + ) + api_call_side_effects.setdefault("get_doorbells", get_doorbells_side_effect) + api_call_side_effects.setdefault( + "get_house_activities", get_house_activities_side_effect + ) + api_call_side_effects.setdefault( + "lock_return_activities", lock_return_activities_side_effect + ) + api_call_side_effects.setdefault( + "unlock_return_activities", unlock_return_activities_side_effect + ) api_instance, entry = await _mock_setup_august_with_api_side_effects( hass, api_call_side_effects, pubnub diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 23ea12a9f82..fe297c97a57 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,6 +1,6 @@ """The tests for the august platform.""" import asyncio -from unittest.mock import patch +from unittest.mock import Mock, patch from aiohttp import ClientResponseError from yalexs.authenticator_common import AuthenticationState @@ -361,19 +361,18 @@ async def test_load_unload(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_load_triggers_ble_discovery(hass: HomeAssistant) -> None: +async def test_load_triggers_ble_discovery( + hass: HomeAssistant, mock_discovery: Mock +) -> None: """Test that loading a lock that supports offline ble operation passes the keys to yalexe_ble.""" august_lock_with_key = await _mock_lock_with_offline_key(hass) august_lock_without_key = await _mock_operative_august_lock_detail(hass) - with patch( - "homeassistant.components.august.discovery_flow.async_create_flow" - ) as mock_discovery: - config_entry = await _create_august_with_devices( - hass, [august_lock_with_key, august_lock_without_key] - ) - await hass.async_block_till_done() + config_entry = await _create_august_with_devices( + hass, [august_lock_with_key, august_lock_without_key] + ) + await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert len(mock_discovery.mock_calls) == 1 From 5657cfa052e2069a9c59a80bfca93785a9b15f0d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Aug 2023 23:26:44 +0200 Subject: [PATCH 025/179] Alexa typing part 2 (#97911) * Alexa typing part 2 * Update homeassistant/components/alexa/intent.py Co-authored-by: Joost Lekkerkerker * Missed type hints * precision * Follow up comment * value * revert abstract class changes * raise NotImplementedError() --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/alexa/entities.py | 18 ++--- homeassistant/components/alexa/intent.py | 68 ++++++++++-------- homeassistant/components/alexa/resources.py | 78 +++++++++++++-------- 3 files changed, 95 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 9a805b43c4f..2931326d430 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Generator, Iterable import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from homeassistant.components import ( alarm_control_panel, @@ -274,22 +274,22 @@ class AlexaEntity: self.entity_conf = config.entity_config.get(entity.entity_id, {}) @property - def entity_id(self): + def entity_id(self) -> str: """Return the Entity ID.""" return self.entity.entity_id - def friendly_name(self): + def friendly_name(self) -> str: """Return the Alexa API friendly name.""" return self.entity_conf.get(CONF_NAME, self.entity.name).translate( TRANSLATION_TABLE ) - def description(self): + def description(self) -> str: """Return the Alexa API description.""" description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) - def alexa_id(self): + def alexa_id(self) -> str: """Return the Alexa API entity id.""" return generate_alexa_id(self.entity.entity_id) @@ -317,7 +317,7 @@ class AlexaEntity: """ raise NotImplementedError - def serialize_properties(self): + def serialize_properties(self) -> Generator[dict[str, Any], None, None]: """Yield each supported property in API format.""" for interface in self.interfaces(): if not interface.properties_proactively_reported(): @@ -325,9 +325,9 @@ class AlexaEntity: yield from interface.serialize_properties() - def serialize_discovery(self): + def serialize_discovery(self) -> dict[str, Any]: """Serialize the entity for discovery.""" - result = { + result: dict[str, Any] = { "displayCategories": self.display_categories(), "cookie": {}, "endpointId": self.alexa_id(), @@ -366,7 +366,7 @@ def async_get_entities( hass: HomeAssistant, config: AbstractConfig ) -> list[AlexaEntity]: """Return all entities that are supported by Alexa.""" - entities = [] + entities: list[AlexaEntity] = [] for state in hass.states.async_all(): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: continue diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 06f76b8806e..ad950803f5c 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -3,8 +3,10 @@ import enum import logging from typing import Any +from aiohttp.web import Response + from homeassistant.components import http -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent from homeassistant.util.decorator import Registry @@ -18,7 +20,7 @@ HANDLERS = Registry() # type: ignore[var-annotated] INTENTS_API_ENDPOINT = "/api/alexa" -class SpeechType(enum.Enum): +class SpeechType(enum.StrEnum): """The Alexa speech types.""" plaintext = "PlainText" @@ -28,7 +30,7 @@ class SpeechType(enum.Enum): SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml} -class CardType(enum.Enum): +class CardType(enum.StrEnum): """The Alexa card types.""" simple = "Simple" @@ -36,12 +38,12 @@ class CardType(enum.Enum): @callback -def async_setup(hass): +def async_setup(hass: HomeAssistant) -> None: """Activate Alexa component.""" hass.http.register_view(AlexaIntentsView) -async def async_setup_intents(hass): +async def async_setup_intents(hass: HomeAssistant) -> None: """Do intents setup. Right now this module does not expose any, but the intent component breaks @@ -60,15 +62,15 @@ class AlexaIntentsView(http.HomeAssistantView): url = INTENTS_API_ENDPOINT name = "api:alexa" - async def post(self, request): + async def post(self, request: http.HomeAssistantRequest) -> Response | bytes: """Handle Alexa.""" - hass = request.app["hass"] - message = await request.json() + hass: HomeAssistant = request.app["hass"] + message: dict[str, Any] = await request.json() _LOGGER.debug("Received Alexa request: %s", message) try: - response = await async_handle_message(hass, message) + response: dict[str, Any] = await async_handle_message(hass, message) return b"" if response is None else self.json(response) except UnknownRequest as err: _LOGGER.warning(str(err)) @@ -99,15 +101,19 @@ class AlexaIntentsView(http.HomeAssistantView): ) -def intent_error_response(hass, message, error): +def intent_error_response( + hass: HomeAssistant, message: dict[str, Any], error: str +) -> dict[str, Any]: """Return an Alexa response that will speak the error message.""" - alexa_intent_info = message.get("request").get("intent") - alexa_response = AlexaResponse(hass, alexa_intent_info) + alexa_intent_info = message["request"].get("intent") + alexa_response = AlexaIntentResponse(hass, alexa_intent_info) alexa_response.add_speech(SpeechType.plaintext, error) return alexa_response.as_dict() -async def async_handle_message(hass, message): +async def async_handle_message( + hass: HomeAssistant, message: dict[str, Any] +) -> dict[str, Any]: """Handle an Alexa intent. Raises: @@ -117,7 +123,7 @@ async def async_handle_message(hass, message): - intent.IntentError """ - req = message.get("request") + req = message["request"] req_type = req["type"] if not (handler := HANDLERS.get(req_type)): @@ -129,7 +135,9 @@ async def async_handle_message(hass, message): @HANDLERS.register("SessionEndedRequest") @HANDLERS.register("IntentRequest") @HANDLERS.register("LaunchRequest") -async def async_handle_intent(hass, message): +async def async_handle_intent( + hass: HomeAssistant, message: dict[str, Any] +) -> dict[str, Any]: """Handle an intent request. Raises: @@ -138,9 +146,9 @@ async def async_handle_intent(hass, message): - intent.IntentError """ - req = message.get("request") + req = message["request"] alexa_intent_info = req.get("intent") - alexa_response = AlexaResponse(hass, alexa_intent_info) + alexa_response = AlexaIntentResponse(hass, alexa_intent_info) if req["type"] == "LaunchRequest": intent_name = ( @@ -187,7 +195,7 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]: # passes the id and name of the nearest possible slot resolution. For # reference to the request object structure, see the Alexa docs: # https://tinyurl.com/ybvm7jhs - resolved_data = {} + resolved_data: dict[str, Any] = {} resolved_data["value"] = request["value"] resolved_data["id"] = "" @@ -226,18 +234,18 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]: return resolved_data -class AlexaResponse: +class AlexaIntentResponse: """Help generating the response for Alexa.""" - def __init__(self, hass, intent_info): + def __init__(self, hass: HomeAssistant, intent_info: dict[str, Any] | None) -> None: """Initialize the response.""" self.hass = hass - self.speech = None - self.card = None - self.reprompt = None - self.session_attributes = {} + self.speech: dict[str, Any] | None = None + self.card: dict[str, Any] | None = None + self.reprompt: dict[str, Any] | None = None + self.session_attributes: dict[str, Any] = {} self.should_end_session = True - self.variables = {} + self.variables: dict[str, Any] = {} # Intent is None if request was a LaunchRequest or SessionEndedRequest if intent_info is not None: @@ -252,7 +260,7 @@ class AlexaResponse: self.variables[_key] = _slot_data["value"] self.variables[_key + "_Id"] = _slot_data["id"] - def add_card(self, card_type, title, content): + def add_card(self, card_type: CardType, title: str, content: str) -> None: """Add a card to the response.""" assert self.card is None @@ -266,7 +274,7 @@ class AlexaResponse: card["content"] = content self.card = card - def add_speech(self, speech_type, text): + def add_speech(self, speech_type: SpeechType, text: str) -> None: """Add speech to the response.""" assert self.speech is None @@ -274,7 +282,7 @@ class AlexaResponse: self.speech = {"type": speech_type.value, key: text} - def add_reprompt(self, speech_type, text): + def add_reprompt(self, speech_type: SpeechType, text) -> None: """Add reprompt if user does not answer.""" assert self.reprompt is None @@ -284,9 +292,9 @@ class AlexaResponse: self.reprompt = {"type": speech_type.value, key: text} - def as_dict(self): + def as_dict(self) -> dict[str, Any]: """Return response in an Alexa valid dict.""" - response = {"shouldEndSession": self.should_end_session} + response: dict[str, Any] = {"shouldEndSession": self.should_end_session} if self.card is not None: response["card"] = self.card diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index e171cf0ebdc..aa242933d8d 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -1,6 +1,9 @@ """Alexa Resources and Assets.""" +from typing import Any + + class AlexaGlobalCatalog: """The Global Alexa catalog. @@ -207,36 +210,40 @@ class AlexaCapabilityResource: https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources """ - def __init__(self, labels): + def __init__(self, labels: list[str]) -> None: """Initialize an Alexa resource.""" self._resource_labels = [] for label in labels: self._resource_labels.append(label) - def serialize_capability_resources(self): + def serialize_capability_resources(self) -> dict[str, list[dict[str, Any]]]: """Return capabilityResources object serialized for an API response.""" return self.serialize_labels(self._resource_labels) - def serialize_configuration(self): + def serialize_configuration(self) -> dict[str, Any]: """Return serialized configuration for an API response. Return ModeResources, PresetResources friendlyNames serialized. """ - return [] + raise NotImplementedError() - def serialize_labels(self, resources): + def serialize_labels(self, resources: list[str]) -> dict[str, list[dict[str, Any]]]: """Return serialized labels for an API response. Returns resource label objects for friendlyNames serialized. """ - labels = [] + labels: list[dict[str, Any]] = [] + label_dict: dict[str, Any] for label in resources: if label in AlexaGlobalCatalog.__dict__.values(): - label = {"@type": "asset", "value": {"assetId": label}} + label_dict = {"@type": "asset", "value": {"assetId": label}} else: - label = {"@type": "text", "value": {"text": label, "locale": "en-US"}} + label_dict = { + "@type": "text", + "value": {"text": label, "locale": "en-US"}, + } - labels.append(label) + labels.append(label_dict) return {"friendlyNames": labels} @@ -247,22 +254,22 @@ class AlexaModeResource(AlexaCapabilityResource): https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources """ - def __init__(self, labels, ordered=False): + def __init__(self, labels: list[str], ordered: bool = False) -> None: """Initialize an Alexa modeResource.""" super().__init__(labels) - self._supported_modes = [] - self._mode_ordered = ordered + self._supported_modes: list[dict[str, Any]] = [] + self._mode_ordered: bool = ordered - def add_mode(self, value, labels): + def add_mode(self, value: str, labels: list[str]) -> None: """Add mode to the supportedModes object.""" self._supported_modes.append({"value": value, "labels": labels}) - def serialize_configuration(self): + def serialize_configuration(self) -> dict[str, Any]: """Return serialized configuration for an API response. Returns configuration for ModeResources friendlyNames serialized. """ - mode_resources = [] + mode_resources: list[dict[str, Any]] = [] for mode in self._supported_modes: result = { "value": mode["value"], @@ -282,10 +289,17 @@ class AlexaPresetResource(AlexaCapabilityResource): https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources """ - def __init__(self, labels, min_value, max_value, precision, unit=None): + def __init__( + self, + labels: list[str], + min_value: int | float, + max_value: int | float, + precision: int | float, + unit: str | None = None, + ) -> None: """Initialize an Alexa presetResource.""" super().__init__(labels) - self._presets = [] + self._presets: list[dict[str, Any]] = [] self._minimum_value = min_value self._maximum_value = max_value self._precision = precision @@ -293,16 +307,16 @@ class AlexaPresetResource(AlexaCapabilityResource): if unit in AlexaGlobalCatalog.__dict__.values(): self._unit_of_measure = unit - def add_preset(self, value, labels): + def add_preset(self, value: int | float, labels: list[str]) -> None: """Add preset to configuration presets array.""" self._presets.append({"value": value, "labels": labels}) - def serialize_configuration(self): + def serialize_configuration(self) -> dict[str, Any]: """Return serialized configuration for an API response. Returns configuration for PresetResources friendlyNames serialized. """ - configuration = { + configuration: dict[str, Any] = { "supportedRange": { "minimumValue": self._minimum_value, "maximumValue": self._maximum_value, @@ -372,26 +386,28 @@ class AlexaSemantics: DIRECTIVE_MODE_SET_MODE = "SetMode" DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" - def __init__(self): + def __init__(self) -> None: """Initialize an Alexa modeResource.""" - self._action_mappings = [] - self._state_mappings = [] + self._action_mappings: list[dict[str, Any]] = [] + self._state_mappings: list[dict[str, Any]] = [] - def _add_action_mapping(self, semantics): + def _add_action_mapping(self, semantics: dict[str, Any]) -> None: """Add action mapping between actions and interface directives.""" self._action_mappings.append(semantics) - def _add_state_mapping(self, semantics): + def _add_state_mapping(self, semantics: dict[str, Any]) -> None: """Add state mapping between states and interface directives.""" self._state_mappings.append(semantics) - def add_states_to_value(self, states, value): + def add_states_to_value(self, states: list[str], value: int | float) -> None: """Add StatesToValue stateMappings.""" self._add_state_mapping( {"@type": self.STATES_TO_VALUE, "states": states, "value": value} ) - def add_states_to_range(self, states, min_value, max_value): + def add_states_to_range( + self, states: list[str], min_value: int | float, max_value: int | float + ) -> None: """Add StatesToRange stateMappings.""" self._add_state_mapping( { @@ -401,7 +417,9 @@ class AlexaSemantics: } ) - def add_action_to_directive(self, actions, directive, payload): + def add_action_to_directive( + self, actions: list[str], directive: str, payload: dict[str, Any] + ) -> None: """Add ActionsToDirective actionMappings.""" self._add_action_mapping( { @@ -411,9 +429,9 @@ class AlexaSemantics: } ) - def serialize_semantics(self): + def serialize_semantics(self) -> dict[str, Any]: """Return semantics object serialized for an API response.""" - semantics = {} + semantics: dict[str, Any] = {} if self._action_mappings: semantics[self.MAPPINGS_ACTION] = self._action_mappings if self._state_mappings: From 987897b0fa2a08cf5d41ab40dae23a8d9b44e5bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 11:28:01 -1000 Subject: [PATCH 026/179] Add support for Yale Doorman to august (#97997) --- .../components/august/binary_sensor.py | 47 +++++--- .../fixtures/lock_with_doorbell.online.json | 100 ++++++++++++++++++ tests/components/august/test_binary_sensor.py | 11 ++ 3 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 tests/components/august/fixtures/lock_with_doorbell.online.json diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index c6f406a5094..2cbeeeee5aa 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -13,7 +13,7 @@ from yalexs.activity import ( ActivityType, ) from yalexs.doorbell import Doorbell, DoorbellDetail -from yalexs.lock import Lock, LockDoorStatus +from yalexs.lock import Lock, LockDetail, LockDoorStatus from yalexs.util import update_lock_detail_from_activity from homeassistant.components.binary_sensor import ( @@ -39,13 +39,16 @@ TIME_TO_RECHECK_DETECTION = timedelta( ) -def _retrieve_online_state(data: AugustData, detail: DoorbellDetail) -> bool: +def _retrieve_online_state( + data: AugustData, detail: DoorbellDetail | LockDetail +) -> bool: """Get the latest state of the sensor.""" # The doorbell will go into standby mode when there is no motion # for a short while. It will wake by itself when needed so we need # to consider is available or we will not report motion or dings - - return detail.is_online or detail.is_standby + if isinstance(detail, DoorbellDetail): + return detail.is_online or detail.is_standby + return detail.bridge_is_online def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: @@ -72,7 +75,7 @@ def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> b return _activity_time_based_state(latest) -def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool: +def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool: assert data.activity_stream is not None latest = data.activity_stream.get_latest_device_activity( detail.device_id, {ActivityType.DOORBELL_DING} @@ -135,15 +138,7 @@ SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription( name="Open", ) - -SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ( - AugustDoorbellBinarySensorEntityDescription( - key="doorbell_ding", - name="Ding", - device_class=BinarySensorDeviceClass.OCCUPANCY, - value_fn=_retrieve_ding_state, - is_time_based=True, - ), +SENSOR_TYPES_VIDEO_DOORBELL = ( AugustDoorbellBinarySensorEntityDescription( key="doorbell_motion", name="Motion", @@ -169,6 +164,17 @@ SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ) +SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ( + AugustDoorbellBinarySensorEntityDescription( + key="doorbell_ding", + name="Ding", + device_class=BinarySensorDeviceClass.OCCUPANCY, + value_fn=_retrieve_ding_state, + is_time_based=True, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -193,8 +199,17 @@ async def async_setup_entry( _LOGGER.debug("Adding sensor class door for %s", door.device_name) entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR)) + if detail.doorbell: + for description in SENSOR_TYPES_DOORBELL: + _LOGGER.debug( + "Adding doorbell sensor class %s for %s", + description.device_class, + door.device_name, + ) + entities.append(AugustDoorbellBinarySensor(data, door, description)) + for doorbell in data.doorbells: - for description in SENSOR_TYPES_DOORBELL: + for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL: _LOGGER.debug( "Adding doorbell sensor class %s for %s", description.device_class, @@ -261,7 +276,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): def __init__( self, data: AugustData, - device: Doorbell, + device: Doorbell | Lock, description: AugustDoorbellBinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" diff --git a/tests/components/august/fixtures/lock_with_doorbell.online.json b/tests/components/august/fixtures/lock_with_doorbell.online.json new file mode 100644 index 00000000000..bb2367d1111 --- /dev/null +++ b/tests/components/august/fixtures/lock_with_doorbell.online.json @@ -0,0 +1,100 @@ +{ + "LockName": "Front Door Lock", + "Type": 7, + "Created": "2017-12-10T03:12:09.210Z", + "Updated": "2017-12-10T03:12:09.210Z", + "LockID": "A6697750D607098BAE8D6BAA11EF8063", + "HouseID": "000000000000", + "HouseName": "My House", + "Calibrated": false, + "skuNumber": "AUG-SL02-M02-S02", + "timeZone": "America/Vancouver", + "battery": 0.88, + "SerialNumber": "X2FSW05DGA", + "LockStatus": { + "status": "locked", + "doorState": "closed", + "dateTime": "2017-12-10T04:48:30.272Z", + "isLockStatusChanged": true, + "valid": true + }, + "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", + "homeKitEnabled": false, + "zWaveEnabled": false, + "isGalileo": false, + "Bridge": { + "_id": "aaacab87f7efxa0015884999", + "mfgBridgeID": "AAGPP102XX", + "deviceModel": "august-doorbell", + "firmwareVersion": "2.3.0-RC153+201711151527", + "operative": true + }, + "keypad": { + "_id": "5bc65c24e6ef2a263e1450a8", + "serialNumber": "K1GXB0054Z", + "lockID": "92412D1B44004595B5DEB134E151A8D3", + "currentFirmwareVersion": "2.27.0", + "battery": {}, + "batteryLevel": "Medium", + "batteryRaw": 170 + }, + "OfflineKeys": { + "created": [], + "loaded": [ + { + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "slot": 1, + "key": "kkk01d4300c1dcxxx1c330f794941111", + "created": "2017-12-10T03:12:09.215Z", + "loaded": "2017-12-10T03:12:54.391Z" + } + ], + "deleted": [], + "loadedhk": [ + { + "key": "kkk01d4300c1dcxxx1c330f794941222", + "slot": 256, + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "created": "2017-12-10T03:12:09.218Z", + "loaded": "2017-12-10T03:12:55.563Z" + } + ] + }, + "parametersToSet": {}, + "users": { + "cccca94e-373e-aaaa-bbbb-333396827777": { + "UserType": "superuser", + "FirstName": "Foo", + "LastName": "Bar", + "identifiers": ["email:foo@bar.com", "phone:+177777777777"], + "imageInfo": { + "original": { + "width": 948, + "height": 949, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + }, + "thumbnail": { + "width": 128, + "height": 128, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + } + } + } + }, + "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", + "ruleHash": {}, + "cameras": [], + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minimumGeofence": 100, + "minGPSAccuracyRequired": 80 + } + } +} diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index f66ba73cebc..2787cdbe23d 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -396,3 +396,14 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() + + +async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None: + """Test creation of a lock with a doorbell.""" + lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json") + await _create_august_with_devices(hass, [lock_one]) + + ding_sensor = hass.states.get( + "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_ding" + ) + assert ding_sensor.state == STATE_OFF From 0f5d423d1e0ae7cb07a4ba4fb695363f08fe0c04 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 7 Aug 2023 23:30:14 +0200 Subject: [PATCH 027/179] Move KNX keyring validation and storage to helper module (#97931) * Move KNX keyfile validation and store to helper module * Rename module and fix tests --- homeassistant/components/knx/config_flow.py | 48 +++++-------------- .../components/knx/helpers/keyring.py | 47 ++++++++++++++++++ tests/components/knx/test_config_flow.py | 4 +- 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/knx/helpers/keyring.py diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 0a405146d9c..8e5783dc2d1 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -3,8 +3,6 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import AsyncGenerator -from pathlib import Path -import shutil from typing import Any, Final import voluptuous as vol @@ -18,15 +16,13 @@ from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner from xknx.io.self_description import request_description from xknx.io.util import validate_ip as xknx_validate_ip -from xknx.secure.keyring import Keyring, XMLInterface, sync_load_keyring +from xknx.secure.keyring import Keyring, XMLInterface -from homeassistant.components.file_upload import process_uploaded_file from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.helpers import selector -from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED from .const import ( @@ -60,6 +56,7 @@ from .const import ( TELEGRAM_LOG_MAX, KNXConfigEntryData, ) +from .helpers.keyring import DEFAULT_KNX_KEYRING_FILENAME, save_uploaded_knxkeys_file from .schema import ia_validator, ip_v4_validator CONF_KNX_GATEWAY: Final = "gateway" @@ -77,7 +74,6 @@ DEFAULT_ENTRY_DATA = KNXConfigEntryData( ) CONF_KEYRING_FILE: Final = "knxkeys_file" -DEFAULT_KNX_KEYRING_FILENAME: Final = "keyring.knxkeys" CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type" CONF_KNX_TUNNELING_TYPE_LABELS: Final = { @@ -499,10 +495,15 @@ class KNXCommonFlow(ABC, FlowHandler): if user_input is not None: password = user_input[CONF_KNX_KNXKEY_PASSWORD] - errors = await self._save_uploaded_knxkeys_file( - uploaded_file_id=user_input[CONF_KEYRING_FILE], - password=password, - ) + try: + self._keyring = await save_uploaded_knxkeys_file( + self.hass, + uploaded_file_id=user_input[CONF_KEYRING_FILE], + password=password, + ) + except InvalidSecureConfiguration: + errors[CONF_KNX_KNXKEY_PASSWORD] = "keyfile_invalid_signature" + if not errors and self._keyring: self.new_entry_data |= KNXConfigEntryData( knxkeys_filename=f"{DOMAIN}/{DEFAULT_KNX_KEYRING_FILENAME}", @@ -711,33 +712,6 @@ class KNXCommonFlow(ABC, FlowHandler): step_id="routing", data_schema=vol.Schema(fields), errors=errors ) - async def _save_uploaded_knxkeys_file( - self, uploaded_file_id: str, password: str - ) -> dict[str, str]: - """Validate the uploaded file and move it to the storage directory. Return errors.""" - - def _process_upload() -> tuple[Keyring | None, dict[str, str]]: - keyring: Keyring | None = None - errors = {} - with process_uploaded_file(self.hass, uploaded_file_id) as file_path: - try: - keyring = sync_load_keyring( - path=file_path, - password=password, - ) - except InvalidSecureConfiguration: - errors[CONF_KNX_KNXKEY_PASSWORD] = "keyfile_invalid_signature" - else: - dest_path = Path(self.hass.config.path(STORAGE_DIR, DOMAIN)) - dest_path.mkdir(exist_ok=True) - dest_file = dest_path / DEFAULT_KNX_KEYRING_FILENAME - shutil.move(file_path, dest_file) - return keyring, errors - - keyring, errors = await self.hass.async_add_executor_job(_process_upload) - self._keyring = keyring - return errors - class KNXConfigFlow(KNXCommonFlow, ConfigFlow, domain=DOMAIN): """Handle a KNX config flow.""" diff --git a/homeassistant/components/knx/helpers/keyring.py b/homeassistant/components/knx/helpers/keyring.py new file mode 100644 index 00000000000..5d1dfea6383 --- /dev/null +++ b/homeassistant/components/knx/helpers/keyring.py @@ -0,0 +1,47 @@ +"""KNX Keyring handler.""" +import logging +from pathlib import Path +import shutil +from typing import Final + +from xknx.exceptions.exception import InvalidSecureConfiguration +from xknx.secure.keyring import Keyring, sync_load_keyring + +from homeassistant.components.file_upload import process_uploaded_file +from homeassistant.core import HomeAssistant +from homeassistant.helpers.storage import STORAGE_DIR + +from ..const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_KNX_KEYRING_FILENAME: Final = "keyring.knxkeys" + + +async def save_uploaded_knxkeys_file( + hass: HomeAssistant, uploaded_file_id: str, password: str +) -> Keyring: + """Validate the uploaded file and move it to the storage directory. + + Return a Keyring object. + Raises InvalidSecureConfiguration if the file or password is invalid. + """ + + def _process_upload() -> Keyring: + with process_uploaded_file(hass, uploaded_file_id) as file_path: + try: + keyring = sync_load_keyring( + path=file_path, + password=password, + ) + except InvalidSecureConfiguration as err: + _LOGGER.debug(err) + raise + dest_path = Path(hass.config.path(STORAGE_DIR, DOMAIN)) + dest_path.mkdir(exist_ok=True) + dest_file = dest_path / DEFAULT_KNX_KEYRING_FILENAME + shutil.move(file_path, dest_file) + return keyring + + return await hass.async_add_executor_job(_process_upload) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 5463892a3ef..f8200214019 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -71,9 +71,9 @@ def fixture_knx_setup(): def patch_file_upload(return_value=FIXTURE_KEYRING, side_effect=None): """Patch file upload. Yields the Keyring instance (return_value).""" with patch( - "homeassistant.components.knx.config_flow.process_uploaded_file" + "homeassistant.components.knx.helpers.keyring.process_uploaded_file" ) as file_upload_mock, patch( - "homeassistant.components.knx.config_flow.sync_load_keyring", + "homeassistant.components.knx.helpers.keyring.sync_load_keyring", return_value=return_value, side_effect=side_effect, ), patch( From d403625e602868a0a8dc6fbc49ac38fdc60314cc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Aug 2023 23:59:56 +0200 Subject: [PATCH 028/179] Alexa typing part 3 (handlers) (#97912) handlers --- homeassistant/components/alexa/handlers.py | 113 +++++++++++---------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 4235d739d22..a37c8b64ab8 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -123,7 +123,7 @@ async def async_api_accept_grant( Async friendly. """ - auth_code = directive.payload["grant"]["code"] + auth_code: str = directive.payload["grant"]["code"] _LOGGER.debug("AcceptGrant code: %s", auth_code) if config.supports_auth: @@ -339,8 +339,8 @@ async def async_api_decrease_color_temp( ) -> AlexaResponse: """Process a decrease color temperature request.""" entity = directive.entity - current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) - max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) + current = int(entity.attributes[light.ATTR_COLOR_TEMP]) + max_mireds = int(entity.attributes[light.ATTR_MAX_MIREDS]) value = min(max_mireds, current + 50) await hass.services.async_call( @@ -363,8 +363,8 @@ async def async_api_increase_color_temp( ) -> AlexaResponse: """Process an increase color temperature request.""" entity = directive.entity - current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) - min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) + current = int(entity.attributes[light.ATTR_COLOR_TEMP]) + min_mireds = int(entity.attributes[light.ATTR_MIN_MIREDS]) value = max(min_mireds, current - 50) await hass.services.async_call( @@ -403,7 +403,7 @@ async def async_api_activate( context=context, ) - payload = { + payload: dict[str, Any] = { "cause": {"type": Cause.VOICE_INTERACTION}, "timestamp": dt_util.utcnow().strftime(DATE_FORMAT), } @@ -432,7 +432,7 @@ async def async_api_deactivate( context=context, ) - payload = { + payload: dict[str, Any] = { "cause": {"type": Cause.VOICE_INTERACTION}, "timestamp": dt_util.utcnow().strftime(DATE_FORMAT), } @@ -509,7 +509,7 @@ async def async_api_set_volume( volume = round(float(directive.payload["volume"] / 100), 2) entity = directive.entity - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, } @@ -554,7 +554,7 @@ async def async_api_select_input( ) raise AlexaInvalidValueError(msg) - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_INPUT_SOURCE: media_input, } @@ -581,7 +581,7 @@ async def async_api_adjust_volume( volume_delta = int(directive.payload["volume"]) entity = directive.entity - current_level = entity.attributes.get(media_player.const.ATTR_MEDIA_VOLUME_LEVEL) + current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL] # read current state try: @@ -591,7 +591,7 @@ async def async_api_adjust_volume( volume = float(max(0, volume_delta + current) / 100) - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, } @@ -631,7 +631,7 @@ async def async_api_adjust_volume_step( if is_default: volume_int = default_steps - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} for _ in range(abs(volume_int)): await hass.services.async_call( @@ -652,7 +652,7 @@ async def async_api_set_mute( """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, } @@ -673,7 +673,7 @@ async def async_api_play( ) -> AlexaResponse: """Process a play request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context @@ -691,7 +691,7 @@ async def async_api_pause( ) -> AlexaResponse: """Process a pause request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context @@ -709,7 +709,7 @@ async def async_api_stop( ) -> AlexaResponse: """Process a stop request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context @@ -727,7 +727,7 @@ async def async_api_next( ) -> AlexaResponse: """Process a next request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context @@ -745,7 +745,7 @@ async def async_api_previous( ) -> AlexaResponse: """Process a previous request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( entity.domain, @@ -758,7 +758,7 @@ async def async_api_previous( return directive.response() -def temperature_from_object(hass, temp_obj, interval=False): +def temperature_from_object(hass: ha.HomeAssistant, temp_obj, interval=False): """Get temperature from Temperature object in requested unit.""" to_unit = hass.config.units.temperature_unit from_unit = UnitOfTemperature.CELSIUS @@ -784,11 +784,11 @@ async def async_api_set_target_temp( ) -> AlexaResponse: """Process a set target temperature request.""" entity = directive.entity - min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) - max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + min_temp = entity.attributes[climate.ATTR_MIN_TEMP] + max_temp = entity.attributes[climate.ATTR_MAX_TEMP] unit = hass.config.units.temperature_unit - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} payload = directive.payload response = directive.response() @@ -848,9 +848,10 @@ async def async_api_adjust_target_temp( context: ha.Context, ) -> AlexaResponse: """Process an adjust target temperature request.""" + data: dict[str, Any] entity = directive.entity - min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) - max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + min_temp = entity.attributes[climate.ATTR_MIN_TEMP] + max_temp = entity.attributes[climate.ATTR_MAX_TEMP] unit = hass.config.units.temperature_unit temp_delta = temperature_from_object( @@ -861,7 +862,7 @@ async def async_api_adjust_target_temp( current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) - if current_target_temp_high and current_target_temp_low: + if current_target_temp_high is not None and current_target_temp_low is not None: target_temp_high = float(current_target_temp_high) + temp_delta if target_temp_high < min_temp or target_temp_high > max_temp: raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp) @@ -891,7 +892,7 @@ async def async_api_adjust_target_temp( } ) else: - target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta + target_temp = float(entity.attributes[ATTR_TEMPERATURE]) + temp_delta if target_temp < min_temp or target_temp > max_temp: raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) @@ -924,11 +925,13 @@ async def async_api_set_thermostat_mode( context: ha.Context, ) -> AlexaResponse: """Process a set thermostat mode request.""" + operation_list: list[str] + entity = directive.entity mode = directive.payload["thermostatMode"] mode = mode if isinstance(mode, str) else mode["value"] - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None) @@ -943,7 +946,7 @@ async def async_api_set_thermostat_mode( data[climate.ATTR_PRESET_MODE] = ha_preset elif mode == "CUSTOM": - operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, []) custom_mode = directive.payload["thermostatMode"]["customName"] custom_mode = next( (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), @@ -959,9 +962,13 @@ async def async_api_set_thermostat_mode( data[climate.ATTR_HVAC_MODE] = custom_mode else: - operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) - ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode} - ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, []) + ha_modes: dict[str, str] = { + k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode + } + ha_mode: str | None = next( + iter(set(ha_modes).intersection(operation_list)), None + ) if ha_mode not in operation_list: msg = f"The requested thermostat mode {mode} is not supported" raise AlexaUnsupportedThermostatModeError(msg) @@ -1006,7 +1013,7 @@ async def async_api_arm( entity = directive.entity service = None arm_state = directive.payload["armState"] - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} if entity.state != STATE_ALARM_DISARMED: msg = "You must disarm the system before you can set the requested arm state." @@ -1026,7 +1033,7 @@ async def async_api_arm( ) # return 0 until alarm integration supports an exit delay - payload = {"exitDelayInSeconds": 0} + payload: dict[str, Any] = {"exitDelayInSeconds": 0} response = directive.response( name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload @@ -1052,7 +1059,7 @@ async def async_api_disarm( ) -> AlexaResponse: """Process a Security Panel Disarm request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} response = directive.response() # Per Alexa Documentation: If you receive a Disarm directive, and the @@ -1094,7 +1101,7 @@ async def async_api_set_mode( instance = directive.instance domain = entity.domain service = None - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} mode = directive.payload["mode"] # Fan Direction @@ -1107,8 +1114,11 @@ async def async_api_set_mode( # Fan preset_mode elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": preset_mode = mode.split(".")[1] - if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get( - fan.ATTR_PRESET_MODES + preset_modes: list[str] | None = entity.attributes.get(fan.ATTR_PRESET_MODES) + if ( + preset_mode != PRESET_MODE_NA + and preset_modes + and preset_mode in preset_modes ): service = fan.SERVICE_SET_PRESET_MODE data[fan.ATTR_PRESET_MODE] = preset_mode @@ -1119,9 +1129,8 @@ async def async_api_set_mode( # Humidifier mode elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": mode = mode.split(".")[1] - if mode != PRESET_MODE_NA and mode in entity.attributes.get( - humidifier.ATTR_AVAILABLE_MODES - ): + modes: list[str] | None = entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) + if mode != PRESET_MODE_NA and modes and mode in modes: service = humidifier.SERVICE_SET_MODE data[humidifier.ATTR_MODE] = mode else: @@ -1194,7 +1203,7 @@ async def async_api_toggle_on( raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) service = fan.SERVICE_OSCILLATE - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, fan.ATTR_OSCILLATING: True, } @@ -1233,7 +1242,7 @@ async def async_api_toggle_off( raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) service = fan.SERVICE_OSCILLATE - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, fan.ATTR_OSCILLATING: False, } @@ -1267,7 +1276,7 @@ async def async_api_set_range( instance = directive.instance domain = entity.domain service = None - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_value = directive.payload["rangeValue"] # Cover Position @@ -1536,7 +1545,7 @@ async def async_api_changechannel( channel = metadata_payload["name"] payload_name = "callSign" - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_CONTENT_ID: channel, media_player.const.ATTR_MEDIA_CONTENT_TYPE: ( @@ -1576,7 +1585,7 @@ async def async_api_skipchannel( channel = int(directive.payload["channelCount"]) entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} if channel < 0: service_media = SERVICE_MEDIA_PREVIOUS_TRACK @@ -1623,7 +1632,7 @@ async def async_api_seek( if media_duration and 0 < int(media_duration) < seek_position: seek_position = media_duration - data = { + data: dict[str, Any] = { ATTR_ENTITY_ID: entity.entity_id, media_player.ATTR_MEDIA_SEEK_POSITION: seek_position, } @@ -1639,7 +1648,9 @@ async def async_api_seek( # convert seconds to milliseconds for StateReport. seek_position = int(seek_position * 1000) - payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]} + payload: dict[str, Any] = { + "properties": [{"name": "positionMilliseconds", "value": seek_position}] + } return directive.response( name="StateReport", namespace="Alexa.SeekController", payload=payload ) @@ -1655,7 +1666,7 @@ async def async_api_set_eq_mode( """Process a SetMode request for EqualizerController.""" mode = directive.payload["mode"] entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) if sound_mode_list and mode.lower() in sound_mode_list: @@ -1701,7 +1712,7 @@ async def async_api_hold( ) -> AlexaResponse: """Process a TimeHoldController Hold request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} if entity.domain == timer.DOMAIN: service = timer.SERVICE_PAUSE @@ -1728,7 +1739,7 @@ async def async_api_resume( ) -> AlexaResponse: """Process a TimeHoldController Resume request.""" entity = directive.entity - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} if entity.domain == timer.DOMAIN: service = timer.SERVICE_START @@ -1773,7 +1784,7 @@ async def async_api_initialize_camera_stream( "Failed to find suitable URL to serve to Alexa" ) from err - payload = { + payload: dict[str, Any] = { "cameraStreams": [ { "uri": f"{external_url}{stream_source}", From 323657e6d709e3406d5ade6d86a82418a5417f0c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Aug 2023 18:14:47 -0400 Subject: [PATCH 029/179] Bump ZHA dependency bellows to 0.35.9 (#97976) Bump bellows to 0.35.8 --- 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 041a93a8ead..29fed3a3c9f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,7 +20,7 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.8", + "bellows==0.35.9", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.102", diff --git a/requirements_all.txt b/requirements_all.txt index 261a3f9a707..67deb3b60ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -500,7 +500,7 @@ beautifulsoup4==4.11.1 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.8 +bellows==0.35.9 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6af5170a7b..478182fd09f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -424,7 +424,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.8 +bellows==0.35.9 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.9 From aff369d64cfa275f2ca00daa1ea393d615ca8911 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Aug 2023 16:23:27 -0600 Subject: [PATCH 030/179] Bump `pyairvisual` to 2023.08.1 (#97999) --- homeassistant/components/airvisual/manifest.json | 2 +- homeassistant/components/airvisual_pro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index f7f509e2593..7934d809287 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -8,5 +8,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"], - "requirements": ["pyairvisual==2022.12.1"] + "requirements": ["pyairvisual==2023.08.1"] } diff --git a/homeassistant/components/airvisual_pro/manifest.json b/homeassistant/components/airvisual_pro/manifest.json index 0859754ba18..32dbc23a421 100644 --- a/homeassistant/components/airvisual_pro/manifest.json +++ b/homeassistant/components/airvisual_pro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["pyairvisual", "pysmb"], - "requirements": ["pyairvisual==2022.12.1"] + "requirements": ["pyairvisual==2023.08.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67deb3b60ca..dfab177d1ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1560,7 +1560,7 @@ pyairnow==1.2.1 # homeassistant.components.airvisual # homeassistant.components.airvisual_pro -pyairvisual==2022.12.1 +pyairvisual==2023.08.1 # homeassistant.components.atag pyatag==0.3.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 478182fd09f..54b7c824a98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1166,7 +1166,7 @@ pyairnow==1.2.1 # homeassistant.components.airvisual # homeassistant.components.airvisual_pro -pyairvisual==2022.12.1 +pyairvisual==2023.08.1 # homeassistant.components.atag pyatag==0.3.5.3 From 0bdae8a382b412fd2aee79a520b13f233eb1f339 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 8 Aug 2023 00:52:54 +0200 Subject: [PATCH 031/179] Use global constant for enphase token (#98002) --- homeassistant/components/enphase_envoy/const.py | 2 -- homeassistant/components/enphase_envoy/coordinator.py | 4 ++-- homeassistant/components/enphase_envoy/diagnostics.py | 10 ++++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index ed829817bf8..029453660fd 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -10,6 +10,4 @@ DOMAIN = "enphase_envoy" PLATFORMS = [Platform.SENSOR] -CONF_TOKEN = "token" - INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index f3ad1705080..de1246fffa5 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -14,14 +14,14 @@ from pyenphase import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util -from .const import CONF_TOKEN, INVALID_AUTH_ERRORS +from .const import INVALID_AUTH_ERRORS SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index a6ce86c4857..1d589cfb176 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -5,10 +5,16 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_UNIQUE_ID, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant -from .const import CONF_TOKEN, DOMAIN +from .const import DOMAIN from .coordinator import EnphaseUpdateCoordinator CONF_TITLE = "title" From 798fb3e31a6ba87358adc93a4c5b772b64451712 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 14:30:47 -1000 Subject: [PATCH 032/179] Bump aiohomekit to 2.6.15 (#98005) changelog: https://github.com/Jc2k/aiohomekit/compare/2.6.14...2.6.15 --- 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 d26b15bdc7a..52a91d42e67 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==2.6.14"], + "requirements": ["aiohomekit==2.6.15"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index dfab177d1ba..2a88c2f0e03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.14 +aiohomekit==2.6.15 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54b7c824a98..df650fc4c7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.14 +aiohomekit==2.6.15 # homeassistant.components.emulated_hue # homeassistant.components.http From 7ea2998b55d5f0df46df58106c3960a8fb2b45dd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 7 Aug 2023 21:22:16 -0500 Subject: [PATCH 033/179] Add wake word integration (#96380) * Add wake component * Add wake support to Wyoming * Add helper function to assist_pipeline (not complete) * Rename wake to wake_word * Fix platform * Use send_event and clean up * Merge wake word into pipeline * Add wake option to async_pipeline_from_audio_stream * Add start/end stages to async_pipeline_from_audio_stream * Add wake timeout * Remove layer in wake_output * Use VAD for wake word timeout * Include audio metadata in wake-start * Remove unnecessary websocket command * wake -> wake_word * Incorporate feedback * Clean up wake_word tests * Add wyoming wake word tests * Add pipeline wake word test * Add last processed state * Fix tests * Add tests for wake word * More tests for the codebot --- CODEOWNERS | 2 + .../components/assist_pipeline/__init__.py | 10 +- .../components/assist_pipeline/error.py | 8 + .../components/assist_pipeline/manifest.json | 2 +- .../components/assist_pipeline/pipeline.py | 224 ++++++++++++++++- .../components/assist_pipeline/vad.py | 93 ++++++- .../assist_pipeline/websocket_api.py | 37 ++- .../components/wake_word/__init__.py | 119 +++++++++ homeassistant/components/wake_word/const.py | 2 + .../components/wake_word/manifest.json | 8 + homeassistant/components/wake_word/models.py | 24 ++ .../components/wyoming/config_flow.py | 9 +- homeassistant/components/wyoming/data.py | 2 + homeassistant/components/wyoming/wake_word.py | 157 ++++++++++++ homeassistant/const.py | 1 + tests/components/assist_pipeline/conftest.py | 61 ++++- .../assist_pipeline/snapshots/test_init.ambr | 111 ++++++++ .../snapshots/test_websocket.ambr | 237 ++++++++++++++++++ tests/components/assist_pipeline/test_init.py | 63 ++++- .../assist_pipeline/test_websocket.py | 218 ++++++++++++++++ tests/components/wake_word/__init__.py | 1 + tests/components/wake_word/common.py | 29 +++ .../wake_word/snapshots/test_init.ambr | 11 + tests/components/wake_word/test_init.py | 226 +++++++++++++++++ tests/components/wyoming/__init__.py | 24 ++ tests/components/wyoming/conftest.py | 29 ++- .../wyoming/snapshots/test_wake_word.ambr | 13 + tests/components/wyoming/test_wake_word.py | 108 ++++++++ 28 files changed, 1802 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/wake_word/__init__.py create mode 100644 homeassistant/components/wake_word/const.py create mode 100644 homeassistant/components/wake_word/manifest.json create mode 100644 homeassistant/components/wake_word/models.py create mode 100644 homeassistant/components/wyoming/wake_word.py create mode 100644 tests/components/wake_word/__init__.py create mode 100644 tests/components/wake_word/common.py create mode 100644 tests/components/wake_word/snapshots/test_init.ambr create mode 100644 tests/components/wake_word/test_init.py create mode 100644 tests/components/wyoming/snapshots/test_wake_word.ambr create mode 100644 tests/components/wyoming/test_wake_word.py diff --git a/CODEOWNERS b/CODEOWNERS index e8617ad7703..084d83b0da1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1373,6 +1373,8 @@ build.json @home-assistant/supervisor /tests/components/vulcan/ @Antoni-Czaplicki /homeassistant/components/wake_on_lan/ @ntilley905 /tests/components/wake_on_lan/ @ntilley905 +/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam +/tests/components/wake_word/ @home-assistant/core @synesthesiam /homeassistant/components/wallbox/ @hesselonline /tests/components/wallbox/ @hesselonline /homeassistant/components/waqi/ @andrey-git diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index 55b192a730a..c2d25da2162 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -18,6 +18,7 @@ from .pipeline import ( PipelineInput, PipelineRun, PipelineStage, + WakeWordSettings, async_create_default_pipeline, async_get_pipeline, async_get_pipelines, @@ -35,6 +36,7 @@ __all__ = ( "PipelineEvent", "PipelineEventType", "PipelineNotFound", + "WakeWordSettings", ) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -57,7 +59,10 @@ async def async_pipeline_from_audio_stream( pipeline_id: str | None = None, conversation_id: str | None = None, tts_audio_output: str | None = None, + wake_word_settings: WakeWordSettings | None = None, device_id: str | None = None, + start_stage: PipelineStage = PipelineStage.STT, + end_stage: PipelineStage = PipelineStage.TTS, ) -> None: """Create an audio pipeline from an audio stream. @@ -72,10 +77,11 @@ async def async_pipeline_from_audio_stream( hass, context=context, pipeline=async_get_pipeline(hass, pipeline_id=pipeline_id), - start_stage=PipelineStage.STT, - end_stage=PipelineStage.TTS, + start_stage=start_stage, + end_stage=end_stage, event_callback=event_callback, tts_audio_output=tts_audio_output, + wake_word_settings=wake_word_settings, ), ) await pipeline_input.validate() diff --git a/homeassistant/components/assist_pipeline/error.py b/homeassistant/components/assist_pipeline/error.py index c5ffdcaf2d3..094913424b6 100644 --- a/homeassistant/components/assist_pipeline/error.py +++ b/homeassistant/components/assist_pipeline/error.py @@ -18,6 +18,14 @@ class PipelineNotFound(PipelineError): """Unspecified pipeline picked.""" +class WakeWordDetectionError(PipelineError): + """Error in wake-word-detection portion of pipeline.""" + + +class WakeWordTimeoutError(WakeWordDetectionError): + """Timeout when wake word was not detected.""" + + class SpeechToTextError(PipelineError): """Error in speech-to-text portion of pipeline.""" diff --git a/homeassistant/components/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index e97ceae5dec..1db415b29d2 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -2,7 +2,7 @@ "domain": "assist_pipeline", "name": "Assist pipeline", "codeowners": ["@balloob", "@synesthesiam"], - "dependencies": ["conversation", "stt", "tts"], + "dependencies": ["conversation", "stt", "tts", "wake_word"], "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", "iot_class": "local_push", "quality_scale": "internal", diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 1be9ddbb14f..3303895eec2 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncIterable, Callable, Iterable +from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable from dataclasses import asdict, dataclass, field from enum import StrEnum import logging @@ -10,7 +10,14 @@ from typing import Any, cast import voluptuous as vol -from homeassistant.components import conversation, media_source, stt, tts, websocket_api +from homeassistant.components import ( + conversation, + media_source, + stt, + tts, + wake_word, + websocket_api, +) from homeassistant.components.tts.media_source import ( generate_media_source_id as tts_generate_media_source_id, ) @@ -39,7 +46,10 @@ from .error import ( PipelineNotFound, SpeechToTextError, TextToSpeechError, + WakeWordDetectionError, + WakeWordTimeoutError, ) +from .vad import VoiceActivityTimeout, VoiceCommandSegmenter _LOGGER = logging.getLogger(__name__) @@ -241,6 +251,8 @@ class PipelineEventType(StrEnum): RUN_START = "run-start" RUN_END = "run-end" + WAKE_WORD_START = "wake_word-start" + WAKE_WORD_END = "wake_word-end" STT_START = "stt-start" STT_END = "stt-end" INTENT_START = "intent-start" @@ -297,12 +309,14 @@ class Pipeline: class PipelineStage(StrEnum): """Stages of a pipeline.""" + WAKE_WORD = "wake_word" STT = "stt" INTENT = "intent" TTS = "tts" PIPELINE_STAGE_ORDER = [ + PipelineStage.WAKE_WORD, PipelineStage.STT, PipelineStage.INTENT, PipelineStage.TTS, @@ -327,6 +341,17 @@ class InvalidPipelineStagesError(PipelineRunValidationError): ) +@dataclass(frozen=True) +class WakeWordSettings: + """Settings for wake word detection.""" + + timeout: float | None = None + """Seconds of silence before detection times out.""" + + audio_seconds_to_buffer: float = 0 + """Seconds of audio to buffer before detection and forward to STT.""" + + @dataclass class PipelineRun: """Running context for a pipeline.""" @@ -341,17 +366,20 @@ class PipelineRun: runner_data: Any | None = None intent_agent: str | None = None tts_audio_output: str | None = None + wake_word_settings: WakeWordSettings | None = None id: str = field(default_factory=ulid_util.ulid) stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False) tts_engine: str = field(init=False) tts_options: dict | None = field(init=False, default=None) + wake_word_engine: str = field(init=False) + wake_word_provider: wake_word.WakeWordDetectionEntity = field(init=False) def __post_init__(self) -> None: """Set language for pipeline.""" self.language = self.pipeline.language or self.hass.config.language - # stt -> intent -> tts + # wake -> stt -> intent -> tts if PIPELINE_STAGE_ORDER.index(self.end_stage) < PIPELINE_STAGE_ORDER.index( self.start_stage ): @@ -393,6 +421,141 @@ class PipelineRun: ) ) + async def prepare_wake_word_detection(self) -> None: + """Prepare wake-word-detection.""" + # Need to add to pipeline store + engine = wake_word.async_default_engine(self.hass) + if engine is None: + raise WakeWordDetectionError( + code="wake-engine-missing", + message="No wake word engine", + ) + + wake_word_provider = wake_word.async_get_wake_word_detection_entity( + self.hass, engine + ) + if wake_word_provider is None: + raise WakeWordDetectionError( + code="wake-provider-missing", + message=f"No wake-word-detection provider for: {engine}", + ) + + self.wake_word_engine = engine + self.wake_word_provider = wake_word_provider + + async def wake_word_detection( + self, + stream: AsyncIterable[bytes], + audio_buffer: list[bytes], + ) -> wake_word.DetectionResult | None: + """Run wake-word-detection portion of pipeline. Returns detection result.""" + metadata_dict = asdict( + stt.SpeechMetadata( + language="", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ) + ) + + # Remove language since it doesn't apply to wake words yet + metadata_dict.pop("language", None) + + self.process_event( + PipelineEvent( + PipelineEventType.WAKE_WORD_START, + { + "engine": self.wake_word_engine, + "metadata": metadata_dict, + }, + ) + ) + + wake_word_settings = self.wake_word_settings or WakeWordSettings() + + wake_word_vad: VoiceActivityTimeout | None = None + if (wake_word_settings.timeout is not None) and ( + wake_word_settings.timeout > 0 + ): + # Use VAD to determine timeout + wake_word_vad = VoiceActivityTimeout(wake_word_settings.timeout) + + # Audio chunk buffer. + audio_bytes_to_buffer = int( + wake_word_settings.audio_seconds_to_buffer * 16000 * 2 + ) + audio_ring_buffer = b"" + + async def timestamped_stream() -> AsyncIterable[tuple[bytes, int]]: + """Yield audio with timestamps (milliseconds since start of stream).""" + nonlocal audio_ring_buffer + + timestamp_ms = 0 + async for chunk in stream: + yield chunk, timestamp_ms + timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz + + # Keeping audio right before wake word detection allows the + # voice command to be spoken immediately after the wake word. + if audio_bytes_to_buffer > 0: + audio_ring_buffer += chunk + if len(audio_ring_buffer) > audio_bytes_to_buffer: + # A proper ring buffer would be far more efficient + audio_ring_buffer = audio_ring_buffer[ + len(audio_ring_buffer) - audio_bytes_to_buffer : + ] + + if (wake_word_vad is not None) and (not wake_word_vad.process(chunk)): + raise WakeWordTimeoutError( + code="wake-word-timeout", message="Wake word was not detected" + ) + + try: + # Detect wake word(s) + result = await self.wake_word_provider.async_process_audio_stream( + timestamped_stream() + ) + + if audio_ring_buffer: + # All audio kept from right before the wake word was detected as + # a single chunk. + audio_buffer.append(audio_ring_buffer) + except WakeWordTimeoutError: + _LOGGER.debug("Timeout during wake word detection") + raise + except Exception as src_error: + _LOGGER.exception("Unexpected error during wake-word-detection") + raise WakeWordDetectionError( + code="wake-stream-failed", + message="Unexpected error during wake-word-detection", + ) from src_error + + _LOGGER.debug("wake-word-detection result %s", result) + + if result is None: + wake_word_output: dict[str, Any] = {} + else: + if result.queued_audio: + # Add audio that was pending at detection + for chunk_ts in result.queued_audio: + audio_buffer.append(chunk_ts[0]) + + wake_word_output = asdict(result) + + # Remove non-JSON fields + wake_word_output.pop("queued_audio", None) + + self.process_event( + PipelineEvent( + PipelineEventType.WAKE_WORD_END, + {"wake_word_output": wake_word_output}, + ) + ) + + return result + async def prepare_speech_to_text(self, metadata: stt.SpeechMetadata) -> None: """Prepare speech-to-text.""" # pipeline.stt_engine can't be None or this function is not called @@ -443,9 +606,21 @@ class PipelineRun: ) try: + segmenter = VoiceCommandSegmenter() + + async def segment_stream( + stream: AsyncIterable[bytes], + ) -> AsyncGenerator[bytes, None]: + """Stop stream when voice command is finished.""" + async for chunk in stream: + if not segmenter.process(chunk): + break + + yield chunk + # Transcribe audio stream result = await self.stt_provider.async_process_audio_stream( - metadata, stream + metadata, segment_stream(stream) ) except Exception as src_error: _LOGGER.exception("Unexpected error during speech-to-text") @@ -663,17 +838,45 @@ class PipelineInput: async def execute(self) -> None: """Run pipeline.""" self.run.start() - current_stage = self.run.start_stage + current_stage: PipelineStage | None = self.run.start_stage + audio_buffer: list[bytes] = [] try: + if current_stage == PipelineStage.WAKE_WORD: + assert self.stt_stream is not None + detect_result = await self.run.wake_word_detection( + self.stt_stream, audio_buffer + ) + if detect_result is None: + # No wake word. Abort the rest of the pipeline. + self.run.end() + return + + current_stage = PipelineStage.STT + # speech-to-text intent_input = self.intent_input if current_stage == PipelineStage.STT: assert self.stt_metadata is not None assert self.stt_stream is not None + + if audio_buffer: + + async def buffered_stream() -> AsyncGenerator[bytes, None]: + for chunk in audio_buffer: + yield chunk + + assert self.stt_stream is not None + async for chunk in self.stt_stream: + yield chunk + + stt_stream = cast(AsyncIterable[bytes], buffered_stream()) + else: + stt_stream = self.stt_stream + intent_input = await self.run.speech_to_text( self.stt_metadata, - self.stt_stream, + stt_stream, ) current_stage = PipelineStage.INTENT @@ -707,7 +910,7 @@ class PipelineInput: async def validate(self) -> None: """Validate pipeline input against start stage.""" - if self.run.start_stage == PipelineStage.STT: + if self.run.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT): if self.run.pipeline.stt_engine is None: raise PipelineRunValidationError( "the pipeline does not support speech-to-text" @@ -741,6 +944,13 @@ class PipelineInput: prepare_tasks = [] + if ( + start_stage_index + <= PIPELINE_STAGE_ORDER.index(PipelineStage.WAKE_WORD) + <= end_stage_index + ): + prepare_tasks.append(self.run.prepare_wake_word_detection()) + if ( start_stage_index <= PIPELINE_STAGE_ORDER.index(PipelineStage.STT) diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index cb19811d650..cae31671a3c 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -88,7 +88,7 @@ class VoiceCommandSegmenter: self.in_command = False def process(self, samples: bytes) -> bool: - """Process a 16-bit 16Khz mono audio samples. + """Process 16-bit 16Khz mono audio samples. Returns False when command is done. """ @@ -148,3 +148,94 @@ class VoiceCommandSegmenter: self._silence_seconds_left = self.silence_seconds return True + + +@dataclass +class VoiceActivityTimeout: + """Detects silence in audio until a timeout is reached.""" + + silence_seconds: float + """Seconds of silence before timeout.""" + + reset_seconds: float = 0.5 + """Seconds of speech before resetting timeout.""" + + vad_mode: int = 3 + """Aggressiveness in filtering out non-speech. 3 is the most aggressive.""" + + vad_frames: int = 480 # 30 ms + """Must be 10, 20, or 30 ms at 16Khz.""" + + _silence_seconds_left: float = 0.0 + """Seconds left before considering voice command as stopped.""" + + _reset_seconds_left: float = 0.0 + """Seconds left before resetting start/stop time counters.""" + + _vad: webrtcvad.Vad = None + _audio_buffer: bytes = field(default_factory=bytes) + _bytes_per_chunk: int = 480 * 2 # 16-bit samples + _seconds_per_chunk: float = 0.03 # 30 ms + + def __post_init__(self) -> None: + """Initialize VAD.""" + self._vad = webrtcvad.Vad(self.vad_mode) + self._bytes_per_chunk = self.vad_frames * 2 + self._seconds_per_chunk = self.vad_frames / _SAMPLE_RATE + self.reset() + + def reset(self) -> None: + """Reset all counters and state.""" + self._audio_buffer = b"" + self._silence_seconds_left = self.silence_seconds + self._reset_seconds_left = self.reset_seconds + + def process(self, samples: bytes) -> bool: + """Process 16-bit 16Khz mono audio samples. + + Returns False when timeout is reached. + """ + self._audio_buffer += samples + + # Process in 10, 20, or 30 ms chunks. + num_chunks = len(self._audio_buffer) // self._bytes_per_chunk + for chunk_idx in range(num_chunks): + chunk_offset = chunk_idx * self._bytes_per_chunk + chunk = self._audio_buffer[ + chunk_offset : chunk_offset + self._bytes_per_chunk + ] + if not self._process_chunk(chunk): + return False + + if num_chunks > 0: + # Remove from buffer + self._audio_buffer = self._audio_buffer[ + num_chunks * self._bytes_per_chunk : + ] + + return True + + def _process_chunk(self, chunk: bytes) -> bool: + """Process a single chunk of 16-bit 16Khz mono audio. + + Returns False when timeout is reached. + """ + if self._vad.is_speech(chunk, _SAMPLE_RATE): + # Speech + self._reset_seconds_left -= self._seconds_per_chunk + if self._reset_seconds_left <= 0: + # Reset timeout + self._silence_seconds_left = self.silence_seconds + else: + # Silence + self._silence_seconds_left -= self._seconds_per_chunk + if self._silence_seconds_left <= 0: + # Timeout reached + return False + + # Slowly build reset counter back up + self._reset_seconds_left = min( + self.reset_seconds, self._reset_seconds_left + self._seconds_per_chunk + ) + + return True diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 4e6d44a8868..bf61b9776e9 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -26,11 +26,12 @@ from .pipeline import ( PipelineInput, PipelineRun, PipelineStage, + WakeWordSettings, async_get_pipeline, ) -from .vad import VoiceCommandSegmenter DEFAULT_TIMEOUT = 30 +DEFAULT_WAKE_WORD_TIMEOUT = 3 _LOGGER = logging.getLogger(__name__) @@ -63,6 +64,18 @@ def async_register_websocket_api(hass: HomeAssistant) -> None: cv.key_value_schemas( "start_stage", { + PipelineStage.WAKE_WORD: vol.Schema( + { + vol.Required("input"): { + vol.Required("sample_rate"): int, + vol.Optional("timeout"): vol.Any(float, int), + vol.Optional("audio_seconds_to_buffer"): vol.Any( + float, int + ), + } + }, + extra=vol.ALLOW_EXTRA, + ), PipelineStage.STT: vol.Schema( {vol.Required("input"): {vol.Required("sample_rate"): int}}, extra=vol.ALLOW_EXTRA, @@ -102,6 +115,7 @@ async def websocket_run( end_stage = PipelineStage(msg["end_stage"]) handler_id: int | None = None unregister_handler: Callable[[], None] | None = None + wake_word_settings: WakeWordSettings | None = None # Arguments to PipelineInput input_args: dict[str, Any] = { @@ -109,24 +123,26 @@ async def websocket_run( "device_id": msg.get("device_id"), } - if start_stage == PipelineStage.STT: + if start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT): # Audio pipeline that will receive audio as binary websocket messages audio_queue: asyncio.Queue[bytes] = asyncio.Queue() incoming_sample_rate = msg["input"]["sample_rate"] + if start_stage == PipelineStage.WAKE_WORD: + wake_word_settings = WakeWordSettings( + timeout=msg["input"].get("timeout", DEFAULT_WAKE_WORD_TIMEOUT), + audio_seconds_to_buffer=msg["input"].get("audio_seconds_to_buffer", 0), + ) + async def stt_stream() -> AsyncGenerator[bytes, None]: state = None - segmenter = VoiceCommandSegmenter() # Yield until we receive an empty chunk while chunk := await audio_queue.get(): - chunk, state = audioop.ratecv( - chunk, 2, 1, incoming_sample_rate, 16000, state - ) - if not segmenter.process(chunk): - # Voice command is finished - break - + if incoming_sample_rate != 16000: + chunk, state = audioop.ratecv( + chunk, 2, 1, incoming_sample_rate, 16000, state + ) yield chunk def handle_binary( @@ -169,6 +185,7 @@ async def websocket_run( "stt_binary_handler_id": handler_id, "timeout": timeout, }, + wake_word_settings=wake_word_settings, ) pipeline_input = PipelineInput(**input_args) diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py new file mode 100644 index 00000000000..f33d06c64da --- /dev/null +++ b/homeassistant/components/wake_word/__init__.py @@ -0,0 +1,119 @@ +"""Provide functionality to wake word.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import AsyncIterable +import logging +from typing import final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import DOMAIN +from .models import DetectionResult, WakeWord + +__all__ = [ + "async_default_engine", + "async_get_wake_word_detection_entity", + "DetectionResult", + "DOMAIN", + "WakeWord", + "WakeWordDetectionEntity", +] + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + + +@callback +def async_default_engine(hass: HomeAssistant) -> str | None: + """Return the domain or entity id of the default engine.""" + return next(iter(hass.states.async_entity_ids(DOMAIN)), None) + + +@callback +def async_get_wake_word_detection_entity( + hass: HomeAssistant, entity_id: str +) -> WakeWordDetectionEntity | None: + """Return wake word entity.""" + component: EntityComponent = hass.data[DOMAIN] + + return component.get_entity(entity_id) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up STT.""" + component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + component.register_shutdown() + + return True + + +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: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class WakeWordDetectionEntity(RestoreEntity): + """Represent a single wake word provider.""" + + _attr_should_poll = False + __last_processed: str | None = None + + @property + @final + def state(self) -> str | None: + """Return the state of the entity.""" + if self.__last_processed is None: + return None + return self.__last_processed + + @property + @abstractmethod + def supported_wake_words(self) -> list[WakeWord]: + """Return a list of supported wake words.""" + + @abstractmethod + async def _async_process_audio_stream( + self, stream: AsyncIterable[tuple[bytes, int]] + ) -> DetectionResult | None: + """Try to detect wake word(s) in an audio stream with timestamps. + + Audio must be 16Khz sample rate with 16-bit mono PCM samples. + """ + + async def async_process_audio_stream( + self, stream: AsyncIterable[tuple[bytes, int]] + ) -> DetectionResult | None: + """Try to detect wake word(s) in an audio stream with timestamps. + + Audio must be 16Khz sample rate with 16-bit mono PCM samples. + """ + self.__last_processed = dt_util.utcnow().isoformat() + self.async_write_ha_state() + return await self._async_process_audio_stream(stream) + + async def async_internal_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if ( + state is not None + and state.state is not None + and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) + ): + self.__last_processed = state.state diff --git a/homeassistant/components/wake_word/const.py b/homeassistant/components/wake_word/const.py new file mode 100644 index 00000000000..fdca6cfab6e --- /dev/null +++ b/homeassistant/components/wake_word/const.py @@ -0,0 +1,2 @@ +"""Wake word constants.""" +DOMAIN = "wake_word" diff --git a/homeassistant/components/wake_word/manifest.json b/homeassistant/components/wake_word/manifest.json new file mode 100644 index 00000000000..7834fad665c --- /dev/null +++ b/homeassistant/components/wake_word/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "wake_word", + "name": "Wake-word detection", + "codeowners": ["@home-assistant/core", "@synesthesiam"], + "documentation": "https://www.home-assistant.io/integrations/wake_word", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/wake_word/models.py b/homeassistant/components/wake_word/models.py new file mode 100644 index 00000000000..1ea154f1393 --- /dev/null +++ b/homeassistant/components/wake_word/models.py @@ -0,0 +1,24 @@ +"""Wake word models.""" +from dataclasses import dataclass + + +@dataclass(frozen=True) +class WakeWord: + """Wake word model.""" + + ww_id: str + name: str + + +@dataclass +class DetectionResult: + """Result of wake word detection.""" + + ww_id: str + """Id of detected wake word""" + + timestamp: int | None + """Timestamp of audio chunk with detected wake word""" + + queued_audio: list[tuple[bytes, int]] | None = None + """Audio chunks that were queued when wake word was detected.""" diff --git a/homeassistant/components/wyoming/config_flow.py b/homeassistant/components/wyoming/config_flow.py index d7d5d0278e8..3fccbaea9c4 100644 --- a/homeassistant/components/wyoming/config_flow.py +++ b/homeassistant/components/wyoming/config_flow.py @@ -50,14 +50,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors={"base": "cannot_connect"}, ) - # ASR = automated speech recognition (STT) + # ASR = automated speech recognition (speech-to-text) asr_installed = [asr for asr in service.info.asr if asr.installed] + + # TTS = text-to-speech tts_installed = [tts for tts in service.info.tts if tts.installed] + # wake-word-detection + wake_installed = [wake for wake in service.info.wake if wake.installed] + if asr_installed: name = asr_installed[0].name elif tts_installed: name = tts_installed[0].name + elif wake_installed: + name = wake_installed[0].name else: return self.async_abort(reason="no_services") diff --git a/homeassistant/components/wyoming/data.py b/homeassistant/components/wyoming/data.py index c2d71835c65..1fe4d60b974 100644 --- a/homeassistant/components/wyoming/data.py +++ b/homeassistant/components/wyoming/data.py @@ -29,6 +29,8 @@ class WyomingService: platforms.append(Platform.STT) if any(tts.installed for tts in info.tts): platforms.append(Platform.TTS) + if any(wake.installed for wake in info.wake): + platforms.append(Platform.WAKE_WORD) self.platforms = platforms @classmethod diff --git a/homeassistant/components/wyoming/wake_word.py b/homeassistant/components/wyoming/wake_word.py new file mode 100644 index 00000000000..0e7fb3c4429 --- /dev/null +++ b/homeassistant/components/wyoming/wake_word.py @@ -0,0 +1,157 @@ +"""Support for Wyoming wake-word-detection services.""" +import asyncio +from collections.abc import AsyncIterable +import logging + +from wyoming.audio import AudioChunk, AudioStart +from wyoming.client import AsyncTcpClient +from wyoming.wake import Detection + +from homeassistant.components import wake_word +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .data import WyomingService +from .error import WyomingError + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Wyoming wake-word-detection.""" + service: WyomingService = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + WyomingWakeWordProvider(config_entry, service), + ] + ) + + +class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): + """Wyoming wake-word-detection provider.""" + + def __init__( + self, + config_entry: ConfigEntry, + service: WyomingService, + ) -> None: + """Set up provider.""" + self.service = service + wake_service = service.info.wake[0] + + self._supported_wake_words = [ + wake_word.WakeWord(ww_id=ww.name, name=ww.name) + for ww in wake_service.models + ] + self._attr_name = wake_service.name + self._attr_unique_id = f"{config_entry.entry_id}-wake_word" + + @property + def supported_wake_words(self) -> list[wake_word.WakeWord]: + """Return a list of supported wake words.""" + return self._supported_wake_words + + async def _async_process_audio_stream( + self, stream: AsyncIterable[tuple[bytes, int]] + ) -> wake_word.DetectionResult | None: + """Try to detect one or more wake words in an audio stream. + + Audio must be 16Khz sample rate with 16-bit mono PCM samples. + """ + + async def next_chunk(): + """Get the next chunk from audio stream.""" + async for chunk_bytes in stream: + return chunk_bytes + + try: + async with AsyncTcpClient(self.service.host, self.service.port) as client: + await client.write_event( + AudioStart( + rate=16000, + width=2, + channels=1, + ).event(), + ) + + # Read audio and wake events in "parallel" + audio_task = asyncio.create_task(next_chunk()) + wake_task = asyncio.create_task(client.read_event()) + pending = {audio_task, wake_task} + + try: + while True: + done, pending = await asyncio.wait( + pending, return_when=asyncio.FIRST_COMPLETED + ) + + if wake_task in done: + event = wake_task.result() + if event is None: + _LOGGER.debug("Connection lost") + break + + if Detection.is_type(event.type): + # Successful detection + detection = Detection.from_event(event) + _LOGGER.info(detection) + + # Retrieve queued audio + queued_audio: list[tuple[bytes, int]] | None = None + if audio_task in pending: + # Save queued audio + await audio_task + pending.remove(audio_task) + queued_audio = [audio_task.result()] + + return wake_word.DetectionResult( + ww_id=detection.name, + timestamp=detection.timestamp, + queued_audio=queued_audio, + ) + + # Next event + wake_task = asyncio.create_task(client.read_event()) + pending.add(wake_task) + + if audio_task in done: + # Forward audio to wake service + chunk_info = audio_task.result() + if chunk_info is None: + break + + chunk_bytes, chunk_timestamp = chunk_info + chunk = AudioChunk( + rate=16000, + width=2, + channels=1, + audio=chunk_bytes, + timestamp=chunk_timestamp, + ) + await client.write_event(chunk.event()) + + # Next chunk + audio_task = asyncio.create_task(next_chunk()) + pending.add(audio_task) + finally: + # Clean up + if audio_task in pending: + # It's critical that we don't cancel the audio task or + # leave it hanging. This would mess up the pipeline STT + # by stopping the audio stream. + await audio_task + pending.remove(audio_task) + + for task in pending: + task.cancel() + + except (OSError, WyomingError) as err: + _LOGGER.exception("Error processing audio stream: %s", err) + + return None diff --git a/homeassistant/const.py b/homeassistant/const.py index a41710f1280..adca3dc965c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -57,6 +57,7 @@ class Platform(StrEnum): TTS = "tts" VACUUM = "vacuum" UPDATE = "update" + WAKE_WORD = "wake_word" WATER_HEATER = "water_heater" WEATHER = "weather" diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 5aa760cc606..0cc18d73e6f 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock import pytest -from homeassistant.components import stt, tts +from homeassistant.components import stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN from homeassistant.components.assist_pipeline.pipeline import ( PipelineData, @@ -174,6 +174,40 @@ class MockSttPlatform(MockPlatform): self.async_get_engine = async_get_engine +class MockWakeWordEntity(wake_word.WakeWordDetectionEntity): + """Mock wake word entity.""" + + fail_process_audio = False + url_path = "wake_word.test" + _attr_name = "test" + + @property + def supported_wake_words(self) -> list[wake_word.WakeWord]: + """Return a list of supported wake words.""" + return [wake_word.WakeWord(ww_id="test_ww", name="Test Wake Word")] + + async def _async_process_audio_stream( + self, stream: AsyncIterable[tuple[bytes, int]] + ) -> wake_word.DetectionResult | None: + """Try to detect wake word(s) in an audio stream with timestamps.""" + async for chunk, timestamp in stream: + if chunk == b"wake word": + return wake_word.DetectionResult( + ww_id=self.supported_wake_words[0].ww_id, + timestamp=timestamp, + queued_audio=[(b"queued audio", 0)], + ) + + # Not detected + return None + + +@pytest.fixture +async def mock_wake_word_provider_entity(hass) -> MockWakeWordEntity: + """Mock wake word provider.""" + return MockWakeWordEntity() + + class MockFlow(ConfigFlow): """Test flow.""" @@ -193,6 +227,7 @@ async def init_supporting_components( mock_stt_provider: MockSttProvider, mock_stt_provider_entity: MockSttProviderEntity, mock_tts_provider: MockTTSProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, config_flow_fixture, ): """Initialize relevant components with empty configs.""" @@ -201,14 +236,18 @@ async def init_supporting_components( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, stt.DOMAIN) + await hass.config_entries.async_forward_entry_setups( + config_entry, [stt.DOMAIN, wake_word.DOMAIN] + ) return True async def async_unload_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Unload up test config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, stt.DOMAIN) + await hass.config_entries.async_unload_platforms( + config_entry, [stt.DOMAIN, wake_word.DOMAIN] + ) return True async def async_setup_entry_stt_platform( @@ -219,6 +258,14 @@ async def init_supporting_components( """Set up test stt platform via config entry.""" async_add_entities([mock_stt_provider_entity]) + async def async_setup_entry_wake_word_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test wake word platform via config entry.""" + async_add_entities([mock_wake_word_provider_entity]) + mock_integration( hass, MockModule( @@ -242,11 +289,19 @@ async def init_supporting_components( async_setup_entry=async_setup_entry_stt_platform, ), ) + mock_platform( + hass, + "test.wake_word", + MockPlatform( + async_setup_entry=async_setup_entry_wake_word_platform, + ), + ) mock_platform(hass, "test.config_flow") assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, tts.DOMAIN, {"tts": {"platform": "test"}}) assert await async_setup_component(hass, stt.DOMAIN, {"stt": {"platform": "test"}}) + # assert await async_setup_component(hass, wake_word.DOMAIN, {"wake_word": {}}) assert await async_setup_component(hass, "media_source", {}) config_entry = MockConfigEntry(domain="test") diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index d8858cec4b6..d0330952f04 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -266,3 +266,114 @@ }), ]) # --- +# name: test_pipeline_from_audio_stream_wake_word + list([ + dict({ + 'data': dict({ + 'language': 'en', + 'pipeline': , + }), + 'type': , + }), + dict({ + 'data': dict({ + 'engine': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': , + 'channel': , + 'codec': , + 'format': , + 'sample_rate': , + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'wake_word_output': dict({ + 'timestamp': 2000, + 'ww_id': 'test_ww', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'engine': 'test', + 'metadata': dict({ + 'bit_rate': , + 'channel': , + 'codec': , + 'format': , + 'language': 'en-US', + 'sample_rate': , + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'stt_output': dict({ + 'text': 'test transcript', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': None, + 'device_id': None, + 'engine': 'homeassistant', + 'intent_input': 'test transcript', + 'language': 'en', + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'code': 'no_intent_match', + }), + 'language': 'en', + 'response_type': 'error', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': "Sorry, I couldn't understand that", + }), + }), + }), + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'engine': 'test', + 'language': 'en-US', + 'tts_input': "Sorry, I couldn't understand that", + 'voice': 'james_earl_jones', + }), + 'type': , + }), + dict({ + 'data': dict({ + 'tts_output': dict({ + 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones", + 'mime_type': 'audio/mpeg', + 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 12a4d766f06..ea642546e6d 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -155,6 +155,243 @@ }), }) # --- +# name: test_audio_pipeline_no_wake_word_engine + dict({ + 'code': 'wake-engine-missing', + 'message': 'No wake word engine', + }) +# --- +# name: test_audio_pipeline_no_wake_word_entity + dict({ + 'code': 'wake-provider-missing', + 'message': 'No wake-word-detection provider for: wake_word.bad-entity-id', + }) +# --- +# name: test_audio_pipeline_with_wake_word + dict({ + 'language': 'en', + 'pipeline': , + 'runner_data': dict({ + 'stt_binary_handler_id': 1, + 'timeout': 30, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word.1 + dict({ + 'engine': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'sample_rate': 16000, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word.2 + dict({ + 'wake_word_output': dict({ + 'queued_audio': None, + 'timestamp': 1000, + 'ww_id': 'test_ww', + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word.3 + dict({ + 'engine': 'test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'language': 'en-US', + 'sample_rate': 16000, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word.4 + dict({ + 'stt_output': dict({ + 'text': 'test transcript', + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word.5 + dict({ + 'conversation_id': None, + 'device_id': None, + 'engine': 'homeassistant', + 'intent_input': 'test transcript', + 'language': 'en', + }) +# --- +# name: test_audio_pipeline_with_wake_word.6 + dict({ + 'intent_output': dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'code': 'no_intent_match', + }), + 'language': 'en', + 'response_type': 'error', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': "Sorry, I couldn't understand that", + }), + }), + }), + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word.7 + dict({ + 'engine': 'test', + 'language': 'en-US', + 'tts_input': "Sorry, I couldn't understand that", + 'voice': 'james_earl_jones', + }) +# --- +# name: test_audio_pipeline_with_wake_word.8 + dict({ + 'tts_output': dict({ + 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones", + 'mime_type': 'audio/mpeg', + 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout + dict({ + 'language': 'en', + 'pipeline': , + 'runner_data': dict({ + 'stt_binary_handler_id': 1, + 'timeout': 30, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.1 + dict({ + 'engine': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'sample_rate': 16000, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.2 + dict({ + 'wake_word_output': dict({ + 'timestamp': 0, + 'ww_id': 'test_ww', + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.3 + dict({ + 'engine': 'test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'language': 'en-US', + 'sample_rate': 16000, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.4 + dict({ + 'stt_output': dict({ + 'text': 'test transcript', + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.5 + dict({ + 'conversation_id': None, + 'device_id': None, + 'engine': 'homeassistant', + 'intent_input': 'test transcript', + 'language': 'en', + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.6 + dict({ + 'intent_output': dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'code': 'no_intent_match', + }), + 'language': 'en', + 'response_type': 'error', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': "Sorry, I couldn't understand that", + }), + }), + }), + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.7 + dict({ + 'engine': 'test', + 'language': 'en-US', + 'tts_input': "Sorry, I couldn't understand that", + 'voice': 'james_earl_jones', + }) +# --- +# name: test_audio_pipeline_with_wake_word_no_timeout.8 + dict({ + 'tts_output': dict({ + 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones", + 'mime_type': 'audio/mpeg', + 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_timeout + dict({ + 'language': 'en', + 'pipeline': , + 'runner_data': dict({ + 'stt_binary_handler_id': 1, + 'timeout': 30, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_timeout.1 + dict({ + 'engine': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'sample_rate': 16000, + }), + }) +# --- +# name: test_audio_pipeline_with_wake_word_timeout.2 + dict({ + 'code': 'wake-word-timeout', + 'message': 'Wake word was not detected', + }) +# --- # name: test_intent_failed dict({ 'language': 'en', diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 392363fc0cc..44e448aa785 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -1,5 +1,6 @@ """Test Voice Assistant init.""" from dataclasses import asdict +import itertools as it from unittest.mock import ANY import pytest @@ -8,10 +9,12 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components import assist_pipeline, stt from homeassistant.core import Context, HomeAssistant -from .conftest import MockSttProvider, MockSttProviderEntity +from .conftest import MockSttProvider, MockSttProviderEntity, MockWakeWordEntity from tests.typing import WebSocketGenerator +BYTES_ONE_SECOND = 16000 * 2 + def process_events(events: list[assist_pipeline.PipelineEvent]) -> list[dict]: """Process events to remove dynamic values.""" @@ -280,3 +283,61 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( ) assert not events + + +async def test_pipeline_from_audio_stream_wake_word( + hass: HomeAssistant, + mock_stt_provider: MockSttProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, + init_components, + snapshot: SnapshotAssertion, +) -> None: + """Test creating a pipeline from an audio stream with wake word.""" + + events = [] + + # [0, 1, ...] + wake_chunk_1 = bytes(it.islice(it.cycle(range(256)), BYTES_ONE_SECOND)) + + # [0, 2, ...] + wake_chunk_2 = bytes(it.islice(it.cycle(range(0, 256, 2)), BYTES_ONE_SECOND)) + + async def audio_data(): + yield wake_chunk_1 # 1 second + yield wake_chunk_2 # 1 second + yield b"wake word" + yield b"part1" + yield b"part2" + yield b"" + + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + Context(), + events.append, + stt.SpeechMetadata( + language="", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + audio_data(), + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + wake_word_settings=assist_pipeline.WakeWordSettings( + audio_seconds_to_buffer=1.5 + ), + ) + + assert process_events(events) == snapshot + + # 1. Half of wake_chunk_1 + all wake_chunk_2 + # 2. queued audio (from mock wake word entity) + # 3. part1 + # 4. part2 + assert len(mock_stt_provider.received) == 4 + + first_chunk = mock_stt_provider.received[0] + assert first_chunk == wake_chunk_1[len(wake_chunk_1) // 2 :] + wake_chunk_2 + + assert mock_stt_provider.received[1:] == [b"queued audio", b"part1", b"part2"] diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 4ebf0a1fb98..1f2b657dcfa 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -167,6 +167,224 @@ async def test_audio_pipeline( assert msg["result"] == {"events": events} +async def test_audio_pipeline_with_wake_word_timeout( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + snapshot: SnapshotAssertion, +) -> None: + """Test timeout from a pipeline run with audio input/output + wake word.""" + events = [] + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": 16000, + "timeout": 1, + }, + } + ) + + # result + msg = await client.receive_json() + assert msg["success"], msg + + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # wake_word + msg = await client.receive_json() + assert msg["event"]["type"] == "wake_word-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # 2 seconds of silence + await client.send_bytes(bytes([1]) + bytes(16000 * 2 * 2)) + + # Time out error + msg = await client.receive_json() + assert msg["event"]["type"] == "error" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + +async def test_audio_pipeline_with_wake_word_no_timeout( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + snapshot: SnapshotAssertion, +) -> None: + """Test events from a pipeline run with audio input/output + wake word with no timeout.""" + events = [] + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": 16000, + "timeout": 0, + }, + } + ) + + # result + msg = await client.receive_json() + assert msg["success"], msg + + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # wake_word + msg = await client.receive_json() + assert msg["event"]["type"] == "wake_word-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # "audio" + await client.send_bytes(bytes([1]) + b"wake word") + + msg = await client.receive_json() + assert msg["event"]["type"] == "wake_word-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # stt + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # End of audio stream (handler id + empty payload) + await client.send_bytes(bytes([1])) + + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # intent + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # text-to-speech + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] is None + events.append(msg["event"]) + + pipeline_data: PipelineData = hass.data[DOMAIN] + pipeline_id = list(pipeline_data.pipeline_runs)[0] + pipeline_run_id = list(pipeline_data.pipeline_runs[pipeline_id])[0] + + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline_debug/get", + "pipeline_id": pipeline_id, + "pipeline_run_id": pipeline_run_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"events": events} + + +async def test_audio_pipeline_no_wake_word_engine( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + snapshot: SnapshotAssertion, +) -> None: + """Test timeout from a pipeline run with audio input/output + wake word.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.wake_word.async_default_engine", return_value=None + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": 16000, + }, + } + ) + + # error + msg = await client.receive_json() + assert not msg["success"] + assert "error" in msg + assert msg["error"] == snapshot + + +async def test_audio_pipeline_no_wake_word_entity( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + snapshot: SnapshotAssertion, +) -> None: + """Test timeout from a pipeline run with audio input/output + wake word.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.wake_word.async_default_engine", + return_value="wake_word.bad-entity-id", + ), patch( + "homeassistant.components.wake_word.async_get_wake_word_detection_entity", + return_value=None, + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": 16000, + }, + } + ) + + # error + msg = await client.receive_json() + assert not msg["success"] + assert "error" in msg + assert msg["error"] == snapshot + + async def test_intent_timeout( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, diff --git a/tests/components/wake_word/__init__.py b/tests/components/wake_word/__init__.py new file mode 100644 index 00000000000..ed2fe81a7fe --- /dev/null +++ b/tests/components/wake_word/__init__.py @@ -0,0 +1 @@ +"""Wake-word-detection tests.""" diff --git a/tests/components/wake_word/common.py b/tests/components/wake_word/common.py new file mode 100644 index 00000000000..f732044bc13 --- /dev/null +++ b/tests/components/wake_word/common.py @@ -0,0 +1,29 @@ +"""Provide common test tools for wake-word-detection.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from pathlib import Path +from typing import Any + +from homeassistant.components import wake_word +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from tests.common import MockPlatform, mock_platform + + +def mock_wake_word_entity_platform( + hass: HomeAssistant, + tmp_path: Path, + integration: str, + async_setup_entry: Callable[ + [HomeAssistant, ConfigEntry, AddEntitiesCallback], + Coroutine[Any, Any, None], + ] + | None = None, +) -> MockPlatform: + """Specialize the mock platform for stt.""" + loaded_platform = MockPlatform(async_setup_entry=async_setup_entry) + mock_platform(hass, f"{integration}.{wake_word.DOMAIN}", loaded_platform) + return loaded_platform diff --git a/tests/components/wake_word/snapshots/test_init.ambr b/tests/components/wake_word/snapshots/test_init.ambr new file mode 100644 index 00000000000..ca6d5d950f0 --- /dev/null +++ b/tests/components/wake_word/snapshots/test_init.ambr @@ -0,0 +1,11 @@ +# serializer version: 1 +# name: test_ws_detect + dict({ + 'event': dict({ + 'timestamp': 2048.0, + 'ww_id': 'test_ww', + }), + 'id': 1, + 'type': 'event', + }) +# --- diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py new file mode 100644 index 00000000000..954cbe6dc8c --- /dev/null +++ b/tests/components/wake_word/test_init.py @@ -0,0 +1,226 @@ +"""Test wake_word component setup.""" +from collections.abc import AsyncIterable, Generator +from pathlib import Path + +import pytest + +from homeassistant.components import wake_word +from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_setup_component + +from .common import mock_wake_word_entity_platform + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, + mock_restore_cache, +) + +TEST_DOMAIN = "test" + +_SAMPLES_PER_CHUNK = 1024 +_BYTES_PER_CHUNK = _SAMPLES_PER_CHUNK * 2 # 16-bit +_MS_PER_CHUNK = (_BYTES_PER_CHUNK // 2) // 16 # 16Khz + + +class MockProviderEntity(wake_word.WakeWordDetectionEntity): + """Mock provider entity.""" + + url_path = "wake_word.test" + _attr_name = "test" + + @property + def supported_wake_words(self) -> list[wake_word.WakeWord]: + """Return a list of supported wake words.""" + return [wake_word.WakeWord(ww_id="test_ww", name="Test Wake Word")] + + async def _async_process_audio_stream( + self, stream: AsyncIterable[tuple[bytes, int]] + ) -> wake_word.DetectionResult | None: + """Try to detect wake word(s) in an audio stream with timestamps.""" + async for _chunk, timestamp in stream: + if timestamp >= 2000: + return wake_word.DetectionResult( + ww_id=self.supported_wake_words[0].ww_id, timestamp=timestamp + ) + + # Not detected + return None + + +@pytest.fixture +def mock_provider_entity() -> MockProviderEntity: + """Test provider entity fixture.""" + return MockProviderEntity() + + +class WakeWordFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, WakeWordFlow): + yield + + +@pytest.fixture(name="setup") +async def setup_fixture( + hass: HomeAssistant, + tmp_path: Path, +) -> MockProviderEntity: + """Set up the test environment.""" + provider = MockProviderEntity() + await mock_config_entry_setup(hass, tmp_path, provider) + + return provider + + +async def mock_config_entry_setup( + hass: HomeAssistant, tmp_path: Path, mock_provider_entity: MockProviderEntity +) -> MockConfigEntry: + """Set up a test provider via config entry.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup( + config_entry, wake_word.DOMAIN + ) + return True + + async def async_unload_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload up test config entry.""" + await hass.config_entries.async_forward_entry_unload( + config_entry, wake_word.DOMAIN + ) + return True + + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + async_unload_entry=async_unload_entry_init, + ), + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test stt platform via config entry.""" + async_add_entities([mock_provider_entity]) + + mock_wake_word_entity_platform( + hass, tmp_path, TEST_DOMAIN, async_setup_entry_platform + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def test_config_entry_unload( + hass: HomeAssistant, tmp_path: Path, mock_provider_entity: MockProviderEntity +) -> None: + """Test we can unload config entry.""" + config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) + assert config_entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_detected_entity( + hass: HomeAssistant, tmp_path: Path, setup: MockProviderEntity +) -> None: + """Test successful detection through entity.""" + + async def three_second_stream(): + timestamp = 0 + while timestamp < 3000: + yield bytes(_BYTES_PER_CHUNK), timestamp + timestamp += _MS_PER_CHUNK + + # Need 2 seconds to trigger + result = await setup.async_process_audio_stream(three_second_stream()) + assert result == wake_word.DetectionResult("test_ww", 2048) + + +async def test_not_detected_entity( + hass: HomeAssistant, setup: MockProviderEntity +) -> None: + """Test unsuccessful detection through entity.""" + + async def one_second_stream(): + timestamp = 0 + while timestamp < 1000: + yield bytes(_BYTES_PER_CHUNK), timestamp + timestamp += _MS_PER_CHUNK + + # Need 2 seconds to trigger + result = await setup.async_process_audio_stream(one_second_stream()) + assert result is None + + +async def test_default_engine_none(hass: HomeAssistant, tmp_path: Path) -> None: + """Test async_default_engine.""" + assert await async_setup_component(hass, wake_word.DOMAIN, {wake_word.DOMAIN: {}}) + await hass.async_block_till_done() + + assert wake_word.async_default_engine(hass) is None + + +async def test_default_engine_entity( + hass: HomeAssistant, tmp_path: Path, mock_provider_entity: MockProviderEntity +) -> None: + """Test async_default_engine.""" + await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) + + assert wake_word.async_default_engine(hass) == f"{wake_word.DOMAIN}.{TEST_DOMAIN}" + + +async def test_get_engine_entity( + hass: HomeAssistant, tmp_path: Path, mock_provider_entity: MockProviderEntity +) -> None: + """Test async_get_speech_to_text_engine.""" + await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) + + assert ( + wake_word.async_get_wake_word_detection_entity(hass, f"{wake_word.DOMAIN}.test") + is mock_provider_entity + ) + + +async def test_restore_state( + hass: HomeAssistant, + tmp_path: Path, + mock_provider_entity: MockProviderEntity, +) -> None: + """Test we restore state in the integration.""" + entity_id = f"{wake_word.DOMAIN}.{TEST_DOMAIN}" + timestamp = "2023-01-01T23:59:59+00:00" + mock_restore_cache(hass, (State(entity_id, timestamp),)) + + config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + state = hass.states.get(entity_id) + assert state + assert state.state == timestamp diff --git a/tests/components/wyoming/__init__.py b/tests/components/wyoming/__init__.py index 3d12d41ce5e..c326228ec8b 100644 --- a/tests/components/wyoming/__init__.py +++ b/tests/components/wyoming/__init__.py @@ -1,4 +1,6 @@ """Tests for the Wyoming integration.""" +import asyncio + from wyoming.info import ( AsrModel, AsrProgram, @@ -7,6 +9,8 @@ from wyoming.info import ( TtsProgram, TtsVoice, TtsVoiceSpeaker, + WakeModel, + WakeProgram, ) TEST_ATTR = Attribution(name="Test", url="http://www.test.com") @@ -49,6 +53,25 @@ TTS_INFO = Info( ) ] ) +WAKE_WORD_INFO = Info( + wake=[ + WakeProgram( + name="Test Wake Word", + description="Test Wake Word", + installed=True, + attribution=TEST_ATTR, + models=[ + WakeModel( + name="Test Model", + description="Test Model", + installed=True, + attribution=TEST_ATTR, + languages=["en-US"], + ) + ], + ) + ] +) EMPTY_INFO = Info() @@ -68,6 +91,7 @@ class MockAsyncTcpClient: async def read_event(self): """Receive.""" + await asyncio.sleep(0) # force context switch return self.responses.pop(0) async def __aenter__(self): diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index 6b4e705914f..2c8081908f7 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -8,7 +8,7 @@ from homeassistant.components import stt from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import STT_INFO, TTS_INFO +from . import STT_INFO, TTS_INFO, WAKE_WORD_INFO from tests.common import MockConfigEntry @@ -52,6 +52,21 @@ def tts_config_entry(hass: HomeAssistant) -> ConfigEntry: return entry +@pytest.fixture +def wake_word_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create a config entry.""" + entry = MockConfigEntry( + domain="wyoming", + data={ + "host": "1.2.3.4", + "port": 1234, + }, + title="Test Wake Word", + ) + entry.add_to_hass(hass) + return entry + + @pytest.fixture async def init_wyoming_stt(hass: HomeAssistant, stt_config_entry: ConfigEntry): """Initialize Wyoming STT.""" @@ -72,6 +87,18 @@ async def init_wyoming_tts(hass: HomeAssistant, tts_config_entry: ConfigEntry): await hass.config_entries.async_setup(tts_config_entry.entry_id) +@pytest.fixture +async def init_wyoming_wake_word( + hass: HomeAssistant, wake_word_config_entry: ConfigEntry +): + """Initialize Wyoming Wake Word.""" + with patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=WAKE_WORD_INFO, + ): + await hass.config_entries.async_setup(wake_word_config_entry.entry_id) + + @pytest.fixture def metadata(hass: HomeAssistant) -> stt.SpeechMetadata: """Get default STT metadata.""" diff --git a/tests/components/wyoming/snapshots/test_wake_word.ambr b/tests/components/wyoming/snapshots/test_wake_word.ambr new file mode 100644 index 00000000000..041112cb6ff --- /dev/null +++ b/tests/components/wyoming/snapshots/test_wake_word.ambr @@ -0,0 +1,13 @@ +# serializer version: 1 +# name: test_streaming_audio + dict({ + 'queued_audio': list([ + tuple( + b'chunk', + 1, + ), + ]), + 'timestamp': 0, + 'ww_id': 'Test Model', + }) +# --- diff --git a/tests/components/wyoming/test_wake_word.py b/tests/components/wyoming/test_wake_word.py new file mode 100644 index 00000000000..cd156c660a8 --- /dev/null +++ b/tests/components/wyoming/test_wake_word.py @@ -0,0 +1,108 @@ +"""Test stt.""" +from __future__ import annotations + +import asyncio +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion +from wyoming.asr import Transcript +from wyoming.wake import Detection + +from homeassistant.components import wake_word +from homeassistant.core import HomeAssistant + +from . import MockAsyncTcpClient + + +async def test_support(hass: HomeAssistant, init_wyoming_wake_word) -> None: + """Test supported properties.""" + state = hass.states.get("wake_word.test_wake_word") + assert state is not None + + entity = wake_word.async_get_wake_word_detection_entity( + hass, "wake_word.test_wake_word" + ) + assert entity is not None + + assert entity.supported_wake_words == [ + wake_word.WakeWord(ww_id="Test Model", name="Test Model") + ] + + +async def test_streaming_audio( + hass: HomeAssistant, init_wyoming_wake_word, snapshot: SnapshotAssertion +) -> None: + """Test streaming audio.""" + entity = wake_word.async_get_wake_word_detection_entity( + hass, "wake_word.test_wake_word" + ) + assert entity is not None + + async def audio_stream(): + yield b"chunk", 0 + + # Delay to force a pending audio chunk + await asyncio.sleep(0.05) + yield b"chunk", 1 + + client_events = [ + Transcript("not a wake word event").event(), + Detection(name="Test Model", timestamp=0).event(), + ] + + with patch( + "homeassistant.components.wyoming.wake_word.AsyncTcpClient", + MockAsyncTcpClient(client_events), + ): + result = await entity.async_process_audio_stream(audio_stream()) + + assert result is not None + assert result == snapshot + + +async def test_streaming_audio_connection_lost( + hass: HomeAssistant, init_wyoming_wake_word +) -> None: + """Test streaming audio and losing connection.""" + entity = wake_word.async_get_wake_word_detection_entity( + hass, "wake_word.test_wake_word" + ) + assert entity is not None + + async def audio_stream(): + # Delay to force a pending audio chunk + await asyncio.sleep(0.05) + yield b"chunk", 1 + + with patch( + "homeassistant.components.wyoming.wake_word.AsyncTcpClient", + MockAsyncTcpClient([None]), + ): + result = await entity.async_process_audio_stream(audio_stream()) + + assert result is None + + +async def test_streaming_audio_oserror( + hass: HomeAssistant, init_wyoming_wake_word +) -> None: + """Test streaming audio and error raising.""" + entity = wake_word.async_get_wake_word_detection_entity( + hass, "wake_word.test_wake_word" + ) + assert entity is not None + + async def audio_stream(): + yield b"chunk1", 1000 + + mock_client = MockAsyncTcpClient( + [Detection(name="Test Model", timestamp=1000).event()] + ) + + with patch( + "homeassistant.components.wyoming.wake_word.AsyncTcpClient", + mock_client, + ), patch.object(mock_client, "read_event", side_effect=OSError("Boom!")): + result = await entity.async_process_audio_stream(audio_stream()) + + assert result is None From 128dadafaed5c49e989c599676743ee66ff01c80 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 7 Aug 2023 22:46:00 -0400 Subject: [PATCH 034/179] Add initial sensors for Enphase Encharge batteries (#97946) --- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 157 +++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 158 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index b6ccbf7e548..c21a0138d21 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==0.14.1"], + "requirements": ["pyenphase==0.15.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 71efba899d2..6bbd8dc89d3 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -6,7 +6,13 @@ from dataclasses import dataclass import datetime import logging -from pyenphase import EnvoyInverter, EnvoySystemConsumption, EnvoySystemProduction +from pyenphase import ( + EnvoyEncharge, + EnvoyEnchargePower, + EnvoyInverter, + EnvoySystemConsumption, + EnvoySystemProduction, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,7 +21,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfEnergy, UnitOfPower +from homeassistant.const import ( + PERCENTAGE, + UnitOfApparentPower, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -181,6 +193,71 @@ CONSUMPTION_SENSORS = ( ) +@dataclass +class EnvoyEnchargeRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEncharge], datetime.datetime | int | float] + + +@dataclass +class EnvoyEnchargeSensorEntityDescription( + SensorEntityDescription, EnvoyEnchargeRequiredKeysMixin +): + """Describes an Envoy Encharge sensor entity.""" + + +@dataclass +class EnvoyEnchargePowerRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnchargePower], int | float] + + +@dataclass +class EnvoyEnchargePowerSensorEntityDescription( + SensorEntityDescription, EnvoyEnchargePowerRequiredKeysMixin +): + """Describes an Envoy Encharge sensor entity.""" + + +ENCHARGE_INVENTORY_SENSORS = ( + EnvoyEnchargeSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda encharge: encharge.temperature, + ), + EnvoyEnchargeSensorEntityDescription( + key=LAST_REPORTED_KEY, + translation_key=LAST_REPORTED_KEY, + native_unit_of_measurement=None, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda encharge: dt_util.utc_from_timestamp(encharge.last_report_date), + ), +) +ENCHARGE_POWER_SENSORS = ( + EnvoyEnchargePowerSensorEntityDescription( + key="soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + value_fn=lambda encharge: encharge.soc, + ), + EnvoyEnchargePowerSensorEntityDescription( + key="apparent_power_mva", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + value_fn=lambda encharge: encharge.apparent_power_mva * 0.001, + ), + EnvoyEnchargePowerSensorEntityDescription( + key="real_power_mw", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + value_fn=lambda encharge: encharge.real_power_mw * 0.001, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -210,6 +287,19 @@ async def async_setup_entry( for inverter in envoy_data.inverters ) + if envoy_data.encharge_inventory: + entities.extend( + EnvoyEnchargeInventoryEntity(coordinator, description, encharge) + for description in ENCHARGE_INVENTORY_SENSORS + for encharge in envoy_data.encharge_inventory + ) + if envoy_data.encharge_power: + entities.extend( + EnvoyEnchargePowerEntity(coordinator, description, encharge) + for description in ENCHARGE_POWER_SENSORS + for encharge in envoy_data.encharge_power + ) + async_add_entities(entities) @@ -314,3 +404,66 @@ class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEnt assert envoy.data.inverters is not None inverter = envoy.data.inverters[self._serial_number] return self.entity_description.value_fn(inverter) + + +class EnvoyEnchargeEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): + """Envoy Encharge sensor entity.""" + + entity_description: EnvoyEnchargeSensorEntityDescription | EnvoyEnchargePowerSensorEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnchargeSensorEntityDescription + | EnvoyEnchargePowerSensorEntityDescription, + serial_number: str, + ) -> None: + """Initialize Encharge entity.""" + self.entity_description = description + self._serial_number = serial_number + envoy_serial_num = coordinator.envoy.serial_number + assert envoy_serial_num is not None + self._attr_unique_id = f"{serial_number}_{description.key}" + assert coordinator.envoy.data is not None + assert coordinator.envoy.data.encharge_inventory is not None + encharge = coordinator.envoy.data.encharge_inventory[self._serial_number] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_number)}, + manufacturer="Enphase", + model="Encharge", + name=f"Encharge {serial_number}", + sw_version=str(encharge.firmware_version), + via_device=(DOMAIN, envoy_serial_num), + ) + super().__init__(coordinator) + + +class EnvoyEnchargeInventoryEntity(EnvoyEnchargeEntity): + """Envoy Encharge inventory entity.""" + + entity_description: EnvoyEnchargeSensorEntityDescription + + @property + def native_value(self) -> int | float | datetime.datetime | None: + """Return the state of the inventory sensors.""" + envoy = self.coordinator.envoy + assert envoy.data is not None + assert envoy.data.encharge_inventory is not None + encharge = envoy.data.encharge_inventory[self._serial_number] + return self.entity_description.value_fn(encharge) + + +class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): + """Envoy Encharge power entity.""" + + entity_description: EnvoyEnchargePowerSensorEntityDescription + + @property + def native_value(self) -> int | float | None: + """Return the state of the power sensors.""" + envoy = self.coordinator.envoy + assert envoy.data is not None + assert envoy.data.encharge_power is not None + encharge = envoy.data.encharge_power[self._serial_number] + return self.entity_description.value_fn(encharge) diff --git a/requirements_all.txt b/requirements_all.txt index 2a88c2f0e03..2450f05709d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==0.14.1 +pyenphase==0.15.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df650fc4c7c..1d8a7f4583d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==0.14.1 +pyenphase==0.15.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 2a80a63ac23364239c458ca0a187b6e929910bed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 22:10:36 -1000 Subject: [PATCH 035/179] Cleanup enphase_envoy sensor inheritance (#98012) --- .../components/enphase_envoy/sensor.py | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 6bbd8dc89d3..37063f5e53f 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -7,6 +7,7 @@ import datetime import logging from pyenphase import ( + EnvoyData, EnvoyEncharge, EnvoyEnchargePower, EnvoyInverter, @@ -303,7 +304,30 @@ async def async_setup_entry( async_add_entities(entities) -class EnvoyEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): +class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): + """Defines a base envoy entity.""" + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Init the envoy base entity.""" + self.entity_description = description + serial_number = coordinator.envoy.serial_number + assert serial_number is not None + self.envoy_serial_num = serial_number + super().__init__(coordinator) + + @property + def data(self) -> EnvoyData: + """Return envoy data.""" + data = self.coordinator.envoy.data + assert data is not None + return data + + +class EnvoyEntity(EnvoyBaseEntity, SensorEntity): """Envoy inverter entity.""" _attr_icon = ICON @@ -315,19 +339,15 @@ class EnvoyEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): description: SensorEntityDescription, ) -> None: """Initialize Envoy entity.""" - self.entity_description = description - envoy_name = coordinator.name - envoy_serial_num = coordinator.envoy.serial_number - assert envoy_serial_num is not None - self._attr_unique_id = f"{envoy_serial_num}_{description.key}" + super().__init__(coordinator, description) + self._attr_unique_id = f"{self.envoy_serial_num}_{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, envoy_serial_num)}, + identifiers={(DOMAIN, self.envoy_serial_num)}, manufacturer="Enphase", model=coordinator.envoy.part_number or "Envoy", - name=envoy_name, + name=coordinator.name, sw_version=str(coordinator.envoy.firmware), ) - super().__init__(coordinator) class EnvoyProductionEntity(EnvoyEntity): @@ -338,10 +358,9 @@ class EnvoyProductionEntity(EnvoyEntity): @property def native_value(self) -> int | None: """Return the state of the sensor.""" - envoy = self.coordinator.envoy - assert envoy.data is not None - assert envoy.data.system_production is not None - return self.entity_description.value_fn(envoy.data.system_production) + system_production = self.data.system_production + assert system_production is not None + return self.entity_description.value_fn(system_production) class EnvoyConsumptionEntity(EnvoyEntity): @@ -352,13 +371,12 @@ class EnvoyConsumptionEntity(EnvoyEntity): @property def native_value(self) -> int | None: """Return the state of the sensor.""" - envoy = self.coordinator.envoy - assert envoy.data is not None - assert envoy.data.system_consumption is not None - return self.entity_description.value_fn(envoy.data.system_consumption) + system_consumption = self.data.system_consumption + assert system_consumption is not None + return self.entity_description.value_fn(system_consumption) -class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): +class EnvoyInverterEntity(EnvoyBaseEntity, SensorEntity): """Envoy inverter entity.""" _attr_icon = ICON @@ -372,10 +390,9 @@ class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEnt serial_number: str, ) -> None: """Initialize Envoy inverter entity.""" - self.entity_description = description + super().__init__(coordinator, description) self._serial_number = serial_number key = description.key - if key == INVERTERS_KEY: # Originally there was only one inverter sensor, so we don't want to # break existing installations by changing the unique_id. @@ -384,32 +401,25 @@ class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEnt # Additional sensors have a unique_id that includes the # sensor key. self._attr_unique_id = f"{serial_number}_{key}" - - envoy_serial_num = coordinator.envoy.serial_number - assert envoy_serial_num is not None self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_number)}, name=f"Inverter {serial_number}", manufacturer="Enphase", model="Inverter", - via_device=(DOMAIN, envoy_serial_num), + via_device=(DOMAIN, self.envoy_serial_num), ) - super().__init__(coordinator) @property def native_value(self) -> datetime.datetime | float: """Return the state of the sensor.""" - envoy = self.coordinator.envoy - assert envoy.data is not None - assert envoy.data.inverters is not None - inverter = envoy.data.inverters[self._serial_number] - return self.entity_description.value_fn(inverter) + inverters = self.data.inverters + assert inverters is not None + return self.entity_description.value_fn(inverters[self._serial_number]) -class EnvoyEnchargeEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): +class EnvoyEnchargeEntity(EnvoyBaseEntity, SensorEntity): """Envoy Encharge sensor entity.""" - entity_description: EnvoyEnchargeSensorEntityDescription | EnvoyEnchargePowerSensorEntityDescription _attr_has_entity_name = True def __init__( @@ -420,23 +430,19 @@ class EnvoyEnchargeEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEnt serial_number: str, ) -> None: """Initialize Encharge entity.""" - self.entity_description = description + super().__init__(coordinator, description) self._serial_number = serial_number - envoy_serial_num = coordinator.envoy.serial_number - assert envoy_serial_num is not None self._attr_unique_id = f"{serial_number}_{description.key}" - assert coordinator.envoy.data is not None - assert coordinator.envoy.data.encharge_inventory is not None - encharge = coordinator.envoy.data.encharge_inventory[self._serial_number] + encharge_inventory = self.data.encharge_inventory + assert encharge_inventory is not None self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_number)}, manufacturer="Enphase", model="Encharge", name=f"Encharge {serial_number}", - sw_version=str(encharge.firmware_version), - via_device=(DOMAIN, envoy_serial_num), + sw_version=str(encharge_inventory[self._serial_number].firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), ) - super().__init__(coordinator) class EnvoyEnchargeInventoryEntity(EnvoyEnchargeEntity): @@ -447,11 +453,9 @@ class EnvoyEnchargeInventoryEntity(EnvoyEnchargeEntity): @property def native_value(self) -> int | float | datetime.datetime | None: """Return the state of the inventory sensors.""" - envoy = self.coordinator.envoy - assert envoy.data is not None - assert envoy.data.encharge_inventory is not None - encharge = envoy.data.encharge_inventory[self._serial_number] - return self.entity_description.value_fn(encharge) + encharge_inventory = self.data.encharge_inventory + assert encharge_inventory is not None + return self.entity_description.value_fn(encharge_inventory[self._serial_number]) class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): @@ -462,8 +466,6 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): @property def native_value(self) -> int | float | None: """Return the state of the power sensors.""" - envoy = self.coordinator.envoy - assert envoy.data is not None - assert envoy.data.encharge_power is not None - encharge = envoy.data.encharge_power[self._serial_number] - return self.entity_description.value_fn(encharge) + encharge_power = self.data.encharge_power + assert encharge_power is not None + return self.entity_description.value_fn(encharge_power[self._serial_number]) From dfdf5928376a6b2452b37a2c63a24089f1776db4 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Tue, 8 Aug 2023 01:16:37 -0700 Subject: [PATCH 036/179] Update prometheus-client to 0.17.1 (#97998) --- homeassistant/components/prometheus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index 8ec332c1daf..cb8defb2ed5 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/prometheus", "iot_class": "assumed_state", "loggers": ["prometheus_client"], - "requirements": ["prometheus-client==0.7.1"] + "requirements": ["prometheus-client==0.17.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2450f05709d..30281d1accd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1450,7 +1450,7 @@ prayer-times-calculator==0.0.6 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus-client==0.7.1 +prometheus-client==0.17.1 # homeassistant.components.proxmoxve proxmoxer==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d8a7f4583d..880e6e15684 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1095,7 +1095,7 @@ praw==7.5.0 prayer-times-calculator==0.0.6 # homeassistant.components.prometheus -prometheus-client==0.7.1 +prometheus-client==0.17.1 # homeassistant.components.hardware # homeassistant.components.recorder From dacfa4b4dcee0a20e2a9f456353ace68813bad9a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Aug 2023 10:32:35 +0200 Subject: [PATCH 037/179] Set up shopping list during onboarding (#97974) Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> --- homeassistant/components/onboarding/views.py | 7 +++++- .../components/shopping_list/config_flow.py | 25 ++++++++++++++----- tests/components/onboarding/test_views.py | 23 +++++++++++++++++ .../shopping_list/test_config_flow.py | 19 +++++++++++--- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 5b47394e0e4..05467e96860 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -193,7 +193,12 @@ class CoreConfigOnboardingView(_BaseOnboardingView): await self._async_mark_done(hass) # Integrations to set up when finishing onboarding - onboard_integrations = ["google_translate", "met", "radio_browser"] + onboard_integrations = [ + "google_translate", + "met", + "radio_browser", + "shopping_list", + ] # pylint: disable-next=import-outside-toplevel from homeassistant.components import hassio diff --git a/homeassistant/components/shopping_list/config_flow.py b/homeassistant/components/shopping_list/config_flow.py index 23f66cecebb..0637dcea390 100644 --- a/homeassistant/components/shopping_list/config_flow.py +++ b/homeassistant/components/shopping_list/config_flow.py @@ -1,23 +1,36 @@ -"""Config flow to configure ShoppingList component.""" -from homeassistant import config_entries +"""Config flow to configure the shopping list integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -class ShoppingListFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Config flow for ShoppingList component.""" +class ShoppingListFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for the shopping list integration.""" VERSION = 1 - 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.""" # Check if already configured await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() if user_input is not None: - return self.async_create_entry(title="Shopping List", data=user_input) + return self.async_create_entry(title="Shopping list", data={}) return self.async_show_form(step_id="user") async_step_import = async_step_user + + async def async_step_onboarding( + self, _: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by onboarding.""" + return await self.async_step_user(user_input={}) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index bbe50f9a810..c888381230c 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -117,6 +117,8 @@ def mock_default_integrations(): "homeassistant.components.met.async_setup_entry", return_value=True ), patch( "homeassistant.components.radio_browser.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.shopping_list.async_setup_entry", return_value=True ): yield @@ -453,6 +455,27 @@ async def test_onboarding_core_sets_up_met( assert len(hass.config_entries.async_entries("met")) == 1 +async def test_onboarding_core_sets_up_shopping_list( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + mock_default_integrations, +) -> None: + """Test finishing the core step set up the shopping list.""" + mock_storage(hass_storage, {"done": [const.STEP_USER]}) + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post("/api/onboarding/core_config") + + assert resp.status == 200 + + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries("shopping_list")) == 1 + + async def test_onboarding_core_sets_up_google_translate( hass: HomeAssistant, hass_storage: dict[str, Any], diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index 4f4ea3f2188..34d74d18046 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -1,8 +1,8 @@ """Test config flow.""" -from homeassistant import data_entry_flow from homeassistant.components.shopping_list.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_import(hass: HomeAssistant) -> None: @@ -11,7 +11,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_user(hass: HomeAssistant) -> None: @@ -21,7 +21,7 @@ async def test_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -32,5 +32,16 @@ async def test_user_confirm(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["result"].data == {} + + +async def test_onboarding_flow(hass: HomeAssistant) -> None: + """Test the onboarding configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "onboarding"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Shopping list" + assert result["data"] == {} From 3859d2e2a6ce42428cecda594344e32a6cbbfd0c Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Tue, 8 Aug 2023 10:35:31 +0200 Subject: [PATCH 038/179] Add edl21 sensor for positive active instantaneous power (#94736) --- homeassistant/components/edl21/sensor.py | 11 ++++++++++- homeassistant/components/edl21/strings.json | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 3ce42198fbd..1cf611db881 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -72,6 +72,15 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( icon="mdi:flash", ), # C=1: Active power + + # D=7: Current value + # E=0: Total + SensorEntityDescription( + key="1-0:1.7.0*255", + translation_key="positive_active_instantaneous_power", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + # C=1: Active energy + # D=8: Time integral 1 # E=0: Total SensorEntityDescription( @@ -100,7 +109,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="1-0:1.17.0*255", translation_key="last_signed_positive_active_energy_total", ), - # C=2: Active power - + # C=2: Active energy - # D=8: Time integral 1 # E=0: Total SensorEntityDescription( diff --git a/homeassistant/components/edl21/strings.json b/homeassistant/components/edl21/strings.json index 43978642943..b23cb8103fa 100644 --- a/homeassistant/components/edl21/strings.json +++ b/homeassistant/components/edl21/strings.json @@ -26,6 +26,9 @@ "firmware_version_number": { "name": "Firmware version number" }, + "positive_active_instantaneous_power": { + "name": "Positive active instantaneous power" + }, "positive_active_energy_total": { "name": "Positive active energy total" }, From 5e020ea354dcc85795173cb529b2f928aaf0810a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Aug 2023 11:02:42 +0200 Subject: [PATCH 039/179] Add is_admin checks to cloud APIs (#97804) --- homeassistant/components/cloud/http_api.py | 9 ++++-- homeassistant/components/http/__init__.py | 1 + homeassistant/components/http/decorators.py | 31 +++++++++++++++++++++ tests/components/cloud/test_http_api.py | 28 ++++++++++++++++++- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/http/decorators.py diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 84c348236d4..00ef4455f3b 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -24,7 +24,7 @@ from homeassistant.components.alexa import ( ) from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.homeassistant import exposed_entities -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, require_admin from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import HomeAssistant @@ -128,7 +128,6 @@ def _handle_cloud_errors( try: result = await handler(view, request, *args, **kwargs) return result - except Exception as err: # pylint: disable=broad-except status, msg = _process_cloud_exception(err, request.path) return view.json_message( @@ -188,6 +187,7 @@ class GoogleActionsSyncView(HomeAssistantView): url = "/api/cloud/google_actions/sync" name = "api:cloud:google_actions/sync" + @require_admin @_handle_cloud_errors async def post(self, request: web.Request) -> web.Response: """Trigger a Google Actions sync.""" @@ -204,6 +204,7 @@ class CloudLoginView(HomeAssistantView): url = "/api/cloud/login" name = "api:cloud:login" + @require_admin @_handle_cloud_errors @RequestDataValidator( vol.Schema({vol.Required("email"): str, vol.Required("password"): str}) @@ -244,6 +245,7 @@ class CloudLogoutView(HomeAssistantView): url = "/api/cloud/logout" name = "api:cloud:logout" + @require_admin @_handle_cloud_errors async def post(self, request: web.Request) -> web.Response: """Handle logout request.""" @@ -262,6 +264,7 @@ class CloudRegisterView(HomeAssistantView): url = "/api/cloud/register" name = "api:cloud:register" + @require_admin @_handle_cloud_errors @RequestDataValidator( vol.Schema( @@ -305,6 +308,7 @@ class CloudResendConfirmView(HomeAssistantView): url = "/api/cloud/resend_confirm" name = "api:cloud:resend_confirm" + @require_admin @_handle_cloud_errors @RequestDataValidator(vol.Schema({vol.Required("email"): str})) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: @@ -324,6 +328,7 @@ class CloudForgotPasswordView(HomeAssistantView): url = "/api/cloud/forgot_password" name = "api:cloud:forgot_password" + @require_admin @_handle_cloud_errors @RequestDataValidator(vol.Schema({vol.Required("email"): str})) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 48ad0cb8752..ff287efb083 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -52,6 +52,7 @@ from .const import ( # noqa: F401 KEY_HASS_USER, ) from .cors import setup_cors +from .decorators import require_admin # noqa: F401 from .forwarded import async_setup_forwarded from .headers import setup_headers from .request_context import current_request, setup_request_context diff --git a/homeassistant/components/http/decorators.py b/homeassistant/components/http/decorators.py new file mode 100644 index 00000000000..45bd34fa49f --- /dev/null +++ b/homeassistant/components/http/decorators.py @@ -0,0 +1,31 @@ +"""Decorators for the Home Assistant API.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from typing import Concatenate, ParamSpec, TypeVar + +from aiohttp.web import Request, Response + +from homeassistant.exceptions import Unauthorized + +from .view import HomeAssistantView + +_HomeAssistantViewT = TypeVar("_HomeAssistantViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + + +def require_admin( + func: Callable[Concatenate[_HomeAssistantViewT, Request, _P], Awaitable[Response]] +) -> Callable[Concatenate[_HomeAssistantViewT, Request, _P], Awaitable[Response]]: + """Home Assistant API decorator to require user to be an admin.""" + + async def with_admin( + self: _HomeAssistantViewT, request: Request, *args: _P.args, **kwargs: _P.kwargs + ) -> Response: + """Check admin and call function.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + + return await func(self, request, *args, **kwargs) + + return with_admin diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index ff79fd1ea77..fc6861f2b49 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,6 +1,7 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from http import HTTPStatus +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp @@ -24,7 +25,7 @@ from . import mock_cloud, mock_cloud_prefs from tests.components.google_assistant import MockConfig from tests.test_util.aiohttp import AiohttpClientMocker -from tests.typing import WebSocketGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/payments/subscription_info" @@ -1207,3 +1208,28 @@ async def test_tts_info( assert response["success"] assert response["result"] == {"languages": [["en-US", "male"], ["en-US", "female"]]} + + +@pytest.mark.parametrize( + ("endpoint", "data"), + [ + ("/api/cloud/forgot_password", {"email": "fake@example.com"}), + ("/api/cloud/google_actions/sync", None), + ("/api/cloud/login", {"email": "fake@example.com", "password": "secret"}), + ("/api/cloud/logout", None), + ("/api/cloud/register", {"email": "fake@example.com", "password": "secret"}), + ("/api/cloud/resend_confirm", {"email": "fake@example.com"}), + ], +) +async def test_api_calls_require_admin( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_read_only_access_token: str, + endpoint: str, + data: dict[str, Any] | None, +) -> None: + """Test cloud APIs endpoints do not work as a normal user.""" + client = await hass_client(hass_read_only_access_token) + resp = await client.post(endpoint, json=data) + + assert resp.status == HTTPStatus.UNAUTHORIZED From 3f542c47fd327e11ec7a2baa1df294970c369178 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Aug 2023 11:07:52 +0200 Subject: [PATCH 040/179] Alexa typing part 4 (capabilities) (#97915) * capabilities * hvacmode * tweaks * name * Corrections * Missed hints * remove unreachabe code --- .../components/alexa/capabilities.py | 357 +++++++++--------- homeassistant/components/alexa/resources.py | 2 +- 2 files changed, 185 insertions(+), 174 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 4954853b95e..042ddfac3d5 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,7 +1,9 @@ """Alexa capabilities.""" from __future__ import annotations +from collections.abc import Generator import logging +from typing import Any from homeassistant.components import ( button, @@ -22,6 +24,7 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, CodeFormat, ) +from homeassistant.components.climate import HVACMode from homeassistant.const import ( ATTR_CODE_FORMAT, ATTR_SUPPORTED_FEATURES, @@ -48,7 +51,7 @@ from homeassistant.const import ( UnitOfTemperature, UnitOfVolume, ) -from homeassistant.core import State +from homeassistant.core import HomeAssistant, State import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util @@ -110,7 +113,9 @@ class AlexaCapability: https://developer.amazon.com/docs/device-apis/message-guide.html """ - supported_locales = {"en-US"} + _resource: AlexaCapabilityResource | None + _semantics: AlexaSemantics | None + supported_locales: set[str] = {"en-US"} def __init__( self, @@ -143,7 +148,7 @@ class AlexaCapability: """Return True if non controllable.""" return self._non_controllable_properties - def get_property(self, name): + def get_property(self, name: str) -> dict[str, Any]: """Read and return a property. Return value should be a dict, or raise UnsupportedProperty. @@ -153,63 +158,60 @@ class AlexaCapability: """ raise UnsupportedProperty(name) - def supports_deactivation(self): + def supports_deactivation(self) -> bool | None: """Applicable only to scenes.""" - return None - def capability_proactively_reported(self): + def capability_proactively_reported(self) -> bool | None: """Return True if the capability is proactively reported. Set properties_proactively_reported() for proactively reported properties. Applicable to DoorbellEventSource. """ - return None - def capability_resources(self): + def capability_resources(self) -> dict[str, list[dict[str, Any]]]: """Return the capability object. Applicable to ToggleController, RangeController, and ModeController interfaces. """ - return [] + return {} - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return the configuration object. Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, and EventDetectionSensor. """ - return [] - def configurations(self): + def configurations(self) -> dict[str, Any] | None: """Return the configurations object. The plural configurations object is different that the singular configuration object. Applicable to EqualizerController interface. """ - return [] - def inputs(self): + def inputs(self) -> list[dict[str, str]] | None: """Applicable only to media players.""" - return [] - def semantics(self): + def semantics(self) -> dict[str, Any] | None: """Return the semantics object. Applicable to ToggleController, RangeController, and ModeController interfaces. """ - return [] - def supported_operations(self): + def supported_operations(self) -> list[str]: """Return the supportedOperations object.""" return [] - def camera_stream_configurations(self): + def camera_stream_configurations(self) -> list[dict[str, Any]] | None: """Applicable only to CameraStreamController.""" - return None - def serialize_discovery(self): + def serialize_discovery(self) -> dict[str, Any]: """Serialize according to the Discovery API.""" - result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + result: dict[str, Any] = { + "type": "AlexaInterface", + "interface": self.name(), + "version": "3", + } if (instance := self.instance) is not None: result["instance"] = instance @@ -255,7 +257,7 @@ class AlexaCapability: return result - def serialize_properties(self): + def serialize_properties(self) -> Generator[dict[str, Any], None, None]: """Return properties serialized for an API response.""" for prop in self.properties_supported(): prop_name = prop["name"] @@ -316,7 +318,7 @@ class Alexa(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa" @@ -346,28 +348,28 @@ class AlexaEndpointHealth(AlexaCapability): "pt-BR", } - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.EndpointHealth" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "connectivity"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "connectivity": raise UnsupportedProperty(name) @@ -402,23 +404,23 @@ class AlexaPowerController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.PowerController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "powerState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "powerState": raise UnsupportedProperty(name) @@ -465,23 +467,23 @@ class AlexaLockController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.LockController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "lockState"}] - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "lockState": raise UnsupportedProperty(name) @@ -519,12 +521,16 @@ class AlexaSceneController(AlexaCapability): "pt-BR", } - def __init__(self, entity, supports_deactivation): + def __init__(self, entity: State, supports_deactivation: bool) -> None: """Initialize the entity.""" + self._supports_deactivation = supports_deactivation super().__init__(entity) - self.supports_deactivation = lambda: supports_deactivation - def name(self): + def supports_deactivation(self) -> bool | None: + """Return True if the Scene controller supports deactivation.""" + return self._supports_deactivation + + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.SceneController" @@ -554,23 +560,23 @@ class AlexaBrightnessController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.BrightnessController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "brightness"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "brightness": raise UnsupportedProperty(name) @@ -603,23 +609,23 @@ class AlexaColorController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ColorController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "color"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "color": raise UnsupportedProperty(name) @@ -657,23 +663,23 @@ class AlexaColorTemperatureController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ColorTemperatureController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "colorTemperatureInKelvin"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "colorTemperatureInKelvin": raise UnsupportedProperty(name) @@ -704,11 +710,11 @@ class AlexaSpeaker(AlexaCapability): "ja-JP", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.Speaker" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" properties = [{"name": "volume"}] @@ -718,15 +724,15 @@ class AlexaSpeaker(AlexaCapability): return properties - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name == "volume": current_level = self.entity.attributes.get( @@ -761,7 +767,7 @@ class AlexaStepSpeaker(AlexaCapability): "it-IT", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.StepSpeaker" @@ -791,11 +797,11 @@ class AlexaPlaybackController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.PlaybackController" - def supported_operations(self): + def supported_operations(self) -> list[str]: """Return the supportedOperations object. Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, @@ -843,24 +849,22 @@ class AlexaInputController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.InputController" - def inputs(self): + def inputs(self) -> list[dict[str, str]] | None: """Return the list of valid supported inputs.""" - source_list = self.entity.attributes.get( + source_list: list[str] = self.entity.attributes.get( media_player.ATTR_INPUT_SOURCE_LIST, [] ) return AlexaInputController.get_valid_inputs(source_list) @staticmethod - def get_valid_inputs(source_list): + def get_valid_inputs(source_list: list[str]) -> list[dict[str, str]]: """Return list of supported inputs.""" - input_list = [] + input_list: list[dict[str, str]] = [] for source in source_list: - if not isinstance(source, str): - continue formatted_source = ( source.lower().replace("-", "").replace("_", "").replace(" ", "") ) @@ -897,50 +901,52 @@ class AlexaTemperatureSensor(AlexaCapability): "pt-BR", } - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.TemperatureSensor" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "temperature"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "temperature": raise UnsupportedProperty(name) - unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - temp = self.entity.state + unit: str = self.entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, self.hass.config.units.temperature_unit + ) + temp: str | None = self.entity.state if self.entity.domain == climate.DOMAIN: unit = self.hass.config.units.temperature_unit temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) - if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): return None try: - temp = float(temp) + temp_float = float(temp) except ValueError: _LOGGER.warning("Invalid temp value %s for %s", temp, self.entity.entity_id) return None # Alexa displays temperatures with one decimal digit, we don't need to do # rounding for presentation here. - return {"value": temp, "scale": API_TEMP_UNITS[unit]} + return {"value": temp_float, "scale": API_TEMP_UNITS[UnitOfTemperature(unit)]} class AlexaContactSensor(AlexaCapability): @@ -972,28 +978,28 @@ class AlexaContactSensor(AlexaCapability): "pt-BR", } - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ContactSensor" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "detectionState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "detectionState": raise UnsupportedProperty(name) @@ -1027,28 +1033,28 @@ class AlexaMotionSensor(AlexaCapability): "pt-BR", } - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.MotionSensor" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "detectionState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "detectionState": raise UnsupportedProperty(name) @@ -1083,16 +1089,16 @@ class AlexaThermostatController(AlexaCapability): "pt-BR", } - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ThermostatController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" properties = [{"name": "thermostatMode"}] supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -1103,15 +1109,15 @@ class AlexaThermostatController(AlexaCapability): properties.append({"name": "upperSetpoint"}) return properties - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if self.entity.state == STATE_UNAVAILABLE: return None @@ -1119,13 +1125,13 @@ class AlexaThermostatController(AlexaCapability): if name == "thermostatMode": preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) + mode: dict[str, str] | str | None if preset in API_THERMOSTAT_PRESETS: mode = API_THERMOSTAT_PRESETS[preset] elif self.entity.state == STATE_UNKNOWN: return None else: - mode = API_THERMOSTAT_MODES.get(self.entity.state) - if mode is None: + if self.entity.state not in API_THERMOSTAT_MODES: _LOGGER.error( "%s (%s) has unsupported state value '%s'", self.entity.entity_id, @@ -1133,6 +1139,7 @@ class AlexaThermostatController(AlexaCapability): self.entity.state, ) raise UnsupportedProperty(name) + mode = API_THERMOSTAT_MODES[HVACMode(self.entity.state)] return mode unit = self.hass.config.units.temperature_unit @@ -1158,7 +1165,7 @@ class AlexaThermostatController(AlexaCapability): return {"value": temp, "scale": API_TEMP_UNITS[unit]} - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return configuration object. Translates climate HVAC_MODES and PRESETS to supported Alexa @@ -1166,8 +1173,8 @@ class AlexaThermostatController(AlexaCapability): ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. """ - supported_modes = [] - hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) + supported_modes: list[str] = [] + hvac_modes = self.entity.attributes[climate.ATTR_HVAC_MODES] for mode in hvac_modes: if thermostat_mode := API_THERMOSTAT_MODES.get(mode): supported_modes.append(thermostat_mode) @@ -1181,7 +1188,7 @@ class AlexaThermostatController(AlexaCapability): # Return False for supportsScheduling until supported with event # listener in handler. - configuration = {"supportsScheduling": False} + configuration: dict[str, Any] = {"supportsScheduling": False} if supported_modes: configuration["supportedModes"] = supported_modes @@ -1210,23 +1217,23 @@ class AlexaPowerLevelController(AlexaCapability): "ja-JP", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.PowerLevelController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "powerLevel"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "powerLevel": raise UnsupportedProperty(name) @@ -1255,28 +1262,28 @@ class AlexaSecurityPanelController(AlexaCapability): "pt-BR", } - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.SecurityPanelController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "armState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "armState": raise UnsupportedProperty(name) @@ -1292,7 +1299,7 @@ class AlexaSecurityPanelController(AlexaCapability): return "ARMED_STAY" return "DISARMED" - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return configuration object with supported authorization types.""" code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) supported = self.entity.attributes[ATTR_SUPPORTED_FEATURES] @@ -1350,29 +1357,31 @@ class AlexaModeController(AlexaCapability): "pt-BR", } - def __init__(self, entity, instance, non_controllable=False): + def __init__( + self, entity: State, instance: str, non_controllable: bool = False + ) -> None: """Initialize the entity.""" AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ModeController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "mode"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "mode": raise UnsupportedProperty(name) @@ -1410,14 +1419,14 @@ class AlexaModeController(AlexaCapability): return None - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return configuration with modeResources.""" if isinstance(self._resource, AlexaCapabilityResource): return self._resource.serialize_configuration() return None - def capability_resources(self): + def capability_resources(self) -> dict[str, list[dict[str, Any]]]: """Return capabilityResources object.""" # Fan Direction Resource @@ -1484,9 +1493,9 @@ class AlexaModeController(AlexaCapability): ) return self._resource.serialize_capability_resources() - return None + return {} - def semantics(self): + def semantics(self) -> dict[str, Any] | None: """Build and return semantics object.""" supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -1569,23 +1578,23 @@ class AlexaRangeController(AlexaCapability): self._resource = None self._semantics = None - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.RangeController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "rangeValue"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "rangeValue": raise UnsupportedProperty(name) @@ -1637,14 +1646,14 @@ class AlexaRangeController(AlexaCapability): return None - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return configuration with presetResources.""" if isinstance(self._resource, AlexaCapabilityResource): return self._resource.serialize_configuration() return None - def capability_resources(self): + def capability_resources(self) -> dict[str, list[dict[str, Any]]]: """Return capabilityResources object.""" # Fan Speed Percentage Resources @@ -1758,9 +1767,9 @@ class AlexaRangeController(AlexaCapability): return self._resource.serialize_capability_resources() - return None + return {} - def semantics(self): + def semantics(self) -> dict[str, Any] | None: """Build and return semantics object.""" supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -1873,29 +1882,31 @@ class AlexaToggleController(AlexaCapability): "pt-BR", } - def __init__(self, entity, instance, non_controllable=False): + def __init__( + self, entity: State, instance: str, non_controllable: bool = False + ) -> None: """Initialize the entity.""" AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ToggleController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "toggleState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "toggleState": raise UnsupportedProperty(name) @@ -1907,7 +1918,7 @@ class AlexaToggleController(AlexaCapability): return None - def capability_resources(self): + def capability_resources(self) -> dict[str, list[dict[str, Any]]]: """Return capabilityResources object.""" # Fan Oscillating Resource @@ -1917,7 +1928,7 @@ class AlexaToggleController(AlexaCapability): ) return self._resource.serialize_capability_resources() - return None + return {} class AlexaChannelController(AlexaCapability): @@ -1945,7 +1956,7 @@ class AlexaChannelController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.ChannelController" @@ -1975,7 +1986,7 @@ class AlexaDoorbellEventSource(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.DoorbellEventSource" @@ -2009,23 +2020,23 @@ class AlexaPlaybackStateReporter(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.PlaybackStateReporter" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "playbackState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "playbackState": raise UnsupportedProperty(name) @@ -2064,7 +2075,7 @@ class AlexaSeekController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.SeekController" @@ -2077,24 +2088,24 @@ class AlexaEventDetectionSensor(AlexaCapability): supported_locales = {"en-US"} - def __init__(self, hass, entity): + def __init__(self, hass: HomeAssistant, entity: State) -> None: """Initialize the entity.""" super().__init__(entity) self.hass = hass - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.EventDetectionSensor" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports.""" return [{"name": "humanPresenceDetectionState"}] - def properties_proactively_reported(self): + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "humanPresenceDetectionState": raise UnsupportedProperty(name) @@ -2119,7 +2130,7 @@ class AlexaEventDetectionSensor(AlexaCapability): return {"value": human_presence} - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return supported detection types.""" return { "detectionMethods": ["AUDIO", "VIDEO"], @@ -2165,11 +2176,11 @@ class AlexaEqualizerController(AlexaCapability): "TV", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.EqualizerController" - def properties_supported(self): + def properties_supported(self) -> list[dict[str, str]]: """Return what properties this entity supports. Either bands, mode or both can be specified. Only mode is supported @@ -2177,11 +2188,11 @@ class AlexaEqualizerController(AlexaCapability): """ return [{"name": "mode"}] - def properties_retrievable(self): + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return True - def get_property(self, name): + def get_property(self, name: str) -> Any: """Read and return a property.""" if name != "mode": raise UnsupportedProperty(name) @@ -2192,7 +2203,7 @@ class AlexaEqualizerController(AlexaCapability): return None - def configurations(self): + def configurations(self) -> dict[str, Any] | None: """Return the sound modes supported in the configurations object.""" configurations = None supported_sound_modes = self.get_valid_inputs( @@ -2204,9 +2215,9 @@ class AlexaEqualizerController(AlexaCapability): return configurations @classmethod - def get_valid_inputs(cls, sound_mode_list): + def get_valid_inputs(cls, sound_mode_list: list[str]) -> list[dict[str, str]]: """Return list of supported inputs.""" - input_list = [] + input_list: list[dict[str, str]] = [] for sound_mode in sound_mode_list: sound_mode = sound_mode.upper() @@ -2224,16 +2235,16 @@ class AlexaTimeHoldController(AlexaCapability): supported_locales = {"en-US"} - def __init__(self, entity, allow_remote_resume=False): + def __init__(self, entity: State, allow_remote_resume: bool = False) -> None: """Initialize the entity.""" super().__init__(entity) self._allow_remote_resume = allow_remote_resume - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.TimeHoldController" - def configuration(self): + def configuration(self) -> dict[str, Any] | None: """Return configuration object. Set allowRemoteResume to True if Alexa can restart the operation on the device. @@ -2267,11 +2278,11 @@ class AlexaCameraStreamController(AlexaCapability): "pt-BR", } - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" return "Alexa.CameraStreamController" - def camera_stream_configurations(self): + def camera_stream_configurations(self) -> list[dict[str, Any]] | None: """Return cameraStreamConfigurations object.""" return [ { diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index aa242933d8d..3606c5401ee 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -399,7 +399,7 @@ class AlexaSemantics: """Add state mapping between states and interface directives.""" self._state_mappings.append(semantics) - def add_states_to_value(self, states: list[str], value: int | float) -> None: + def add_states_to_value(self, states: list[str], value: Any) -> None: """Add StatesToValue stateMappings.""" self._add_state_mapping( {"@type": self.STATES_TO_VALUE, "states": states, "value": value} From 1ee0c907b08958c222ced4a257e9d1913a2ff856 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 11:38:01 +0200 Subject: [PATCH 041/179] Improve OTBR factory reset (#98017) Co-authored-by: Stefan Agner --- homeassistant/components/otbr/util.py | 14 ++++ .../components/otbr/websocket_api.py | 4 +- tests/components/otbr/test_util.py | 77 +++++++++++++++++++ tests/components/otbr/test_websocket_api.py | 18 ++--- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 2d6217ea585..4d6efb9a9f0 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine import dataclasses from functools import wraps +import logging from typing import Any, Concatenate, ParamSpec, TypeVar, cast import python_otbr_api @@ -27,6 +28,8 @@ from .const import DOMAIN _R = TypeVar("_R") _P = ParamSpec("_P") +_LOGGER = logging.getLogger(__name__) + INFO_URL_SKY_CONNECT = ( "https://skyconnect.home-assistant.io/multiprotocol-channel-missmatch" ) @@ -68,6 +71,17 @@ class OTBRData: api: python_otbr_api.OTBR entry_id: str + @_handle_otbr_error + async def factory_reset(self) -> None: + """Reset the router.""" + try: + await self.api.factory_reset() + except python_otbr_api.FactoryResetNotSupportedError: + _LOGGER.warning( + "OTBR does not support factory reset, attempting to delete dataset" + ) + await self.delete_active_dataset() + @_handle_otbr_error async def set_enabled(self, enabled: bool) -> None: """Enable or disable the router.""" diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index 3b631057529..9b57cd8ebd1 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -88,9 +88,9 @@ async def websocket_create_network( return try: - await data.delete_active_dataset() + await data.factory_reset() except HomeAssistantError as exc: - connection.send_error(msg["id"], "delete_active_dataset_failed", str(exc)) + connection.send_error(msg["id"], "factory_reset_failed", str(exc)) return try: diff --git a/tests/components/otbr/test_util.py b/tests/components/otbr/test_util.py index f8ed79b91ee..171a607d200 100644 --- a/tests/components/otbr/test_util.py +++ b/tests/components/otbr/test_util.py @@ -1,7 +1,12 @@ """Test OTBR Utility functions.""" +from unittest.mock import patch + +import pytest +import python_otbr_api from homeassistant.components import otbr from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError OTBR_MULTIPAN_URL = "http://core-silabs-multiprotocol:8081" OTBR_NON_MULTIPAN_URL = "/dev/ttyAMA1" @@ -23,3 +28,75 @@ async def test_get_allowed_channel( # OTBR no multipan + multipan using channel 15 -> no restriction multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 assert await otbr.util.get_allowed_channel(hass, OTBR_NON_MULTIPAN_URL) is None + + +async def test_factory_reset(hass: HomeAssistant, otbr_config_entry_multipan) -> None: + """Test factory_reset.""" + data: otbr.OTBRData = hass.data[otbr.DOMAIN] + + with patch("python_otbr_api.OTBR.factory_reset") as factory_reset_mock, patch( + "python_otbr_api.OTBR.delete_active_dataset" + ) as delete_active_dataset_mock: + await data.factory_reset() + + delete_active_dataset_mock.assert_not_called() + factory_reset_mock.assert_called_once_with() + + +async def test_factory_reset_not_supported( + hass: HomeAssistant, otbr_config_entry_multipan +) -> None: + """Test factory_reset.""" + data: otbr.OTBRData = hass.data[otbr.DOMAIN] + + with patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.FactoryResetNotSupportedError, + ) as factory_reset_mock, patch( + "python_otbr_api.OTBR.delete_active_dataset" + ) as delete_active_dataset_mock: + await data.factory_reset() + + delete_active_dataset_mock.assert_called_once_with() + factory_reset_mock.assert_called_once_with() + + +async def test_factory_reset_error_1( + hass: HomeAssistant, otbr_config_entry_multipan +) -> None: + """Test factory_reset.""" + data: otbr.OTBRData = hass.data[otbr.DOMAIN] + + with patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.OTBRError, + ) as factory_reset_mock, patch( + "python_otbr_api.OTBR.delete_active_dataset" + ) as delete_active_dataset_mock, pytest.raises( + HomeAssistantError + ): + await data.factory_reset() + + delete_active_dataset_mock.assert_not_called() + factory_reset_mock.assert_called_once_with() + + +async def test_factory_reset_error_2( + hass: HomeAssistant, otbr_config_entry_multipan +) -> None: + """Test factory_reset.""" + data: otbr.OTBRData = hass.data[otbr.DOMAIN] + + with patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.FactoryResetNotSupportedError, + ) as factory_reset_mock, patch( + "python_otbr_api.OTBR.delete_active_dataset", + side_effect=python_otbr_api.OTBRError, + ) as delete_active_dataset_mock, pytest.raises( + HomeAssistantError + ): + await data.factory_reset() + + delete_active_dataset_mock.assert_called_once_with() + factory_reset_mock.assert_called_once_with() diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index b5dd7aa62c4..d62213ce78b 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -87,8 +87,8 @@ async def test_create_network( with patch( "python_otbr_api.OTBR.create_active_dataset" ) as create_dataset_mock, patch( - "python_otbr_api.OTBR.delete_active_dataset" - ) as delete_dataset_mock, patch( + "python_otbr_api.OTBR.factory_reset" + ) as factory_reset_mock, patch( "python_otbr_api.OTBR.set_enabled" ) as set_enabled_mock, patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 @@ -104,7 +104,7 @@ async def test_create_network( create_dataset_mock.assert_called_once_with( python_otbr_api.models.ActiveDataSet(channel=15, network_name="home-assistant") ) - delete_dataset_mock.assert_called_once_with() + factory_reset_mock.assert_called_once_with() assert len(set_enabled_mock.mock_calls) == 2 assert set_enabled_mock.mock_calls[0][1][0] is False assert set_enabled_mock.mock_calls[1][1][0] is True @@ -157,7 +157,7 @@ async def test_create_network_fails_2( ), patch( "python_otbr_api.OTBR.create_active_dataset", side_effect=python_otbr_api.OTBRError, - ), patch("python_otbr_api.OTBR.delete_active_dataset"): + ), patch("python_otbr_api.OTBR.factory_reset"): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -178,7 +178,7 @@ async def test_create_network_fails_3( ), patch( "python_otbr_api.OTBR.create_active_dataset", ), patch( - "python_otbr_api.OTBR.delete_active_dataset" + "python_otbr_api.OTBR.factory_reset" ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -200,7 +200,7 @@ async def test_create_network_fails_4( "python_otbr_api.OTBR.get_active_dataset_tlvs", side_effect=python_otbr_api.OTBRError, ), patch( - "python_otbr_api.OTBR.delete_active_dataset" + "python_otbr_api.OTBR.factory_reset" ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -219,7 +219,7 @@ async def test_create_network_fails_5( with patch("python_otbr_api.OTBR.set_enabled"), patch( "python_otbr_api.OTBR.create_active_dataset" ), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch( - "python_otbr_api.OTBR.delete_active_dataset" + "python_otbr_api.OTBR.factory_reset" ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -238,14 +238,14 @@ async def test_create_network_fails_6( with patch("python_otbr_api.OTBR.set_enabled"), patch( "python_otbr_api.OTBR.create_active_dataset" ), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch( - "python_otbr_api.OTBR.delete_active_dataset", + "python_otbr_api.OTBR.factory_reset", side_effect=python_otbr_api.OTBRError, ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() assert not msg["success"] - assert msg["error"]["code"] == "delete_active_dataset_failed" + assert msg["error"]["code"] == "factory_reset_failed" async def test_set_network( From 0614702f98f6cf077ea262376e844dc159b9570c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Aug 2023 11:48:50 +0200 Subject: [PATCH 042/179] Alexa typing part 5 (smart_home) (#97918) * smart_home * Fix test_disabled * Remove unused type ignore --- homeassistant/components/alexa/smart_home.py | 62 +++++++++++++------- homeassistant/components/cloud/client.py | 2 +- tests/components/alexa/test_smart_home.py | 19 ++---- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 3f8932a48bc..288f6adcc15 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,7 +1,13 @@ """Support for alexa Smart Home Skill API.""" import logging +from typing import Any + +from aiohttp import web +from yarl import URL from homeassistant import core +from homeassistant.auth.models import User +from homeassistant.components.http import HomeAssistantRequest from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import Context, HomeAssistant @@ -23,15 +29,16 @@ from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS from .state_report import AlexaDirective -SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" - _LOGGER = logging.getLogger(__name__) +SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" class AlexaConfig(AbstractConfig): """Alexa config.""" - def __init__(self, hass, config): + _auth: Auth | None + + def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: """Initialize Alexa config.""" super().__init__(hass) self._config = config @@ -42,37 +49,37 @@ class AlexaConfig(AbstractConfig): self._auth = None @property - def supports_auth(self): + def supports_auth(self) -> bool: """Return if config supports auth.""" return self._auth is not None @property - def should_report_state(self): + def should_report_state(self) -> bool: """Return if we should proactively report states.""" return self._auth is not None and self.authorized @property - def endpoint(self): + def endpoint(self) -> str | URL | None: """Endpoint for report state.""" return self._config.get(CONF_ENDPOINT) @property - def entity_config(self): + def entity_config(self) -> dict[str, Any]: """Return entity config.""" return self._config.get(CONF_ENTITY_CONFIG) or {} @property - def locale(self): + def locale(self) -> str | None: """Return config locale.""" return self._config.get(CONF_LOCALE) @core.callback - def user_identifier(self): + def user_identifier(self) -> str: """Return an identifier for the user that represents this config.""" return "" @core.callback - def should_expose(self, entity_id): + def should_expose(self, entity_id: str) -> bool: """If an entity should be exposed.""" if not self._config[CONF_FILTER].empty_filter: return self._config[CONF_FILTER](entity_id) @@ -88,16 +95,19 @@ class AlexaConfig(AbstractConfig): return not auxiliary_entity @core.callback - def async_invalidate_access_token(self): + def async_invalidate_access_token(self) -> None: """Invalidate access token.""" + assert self._auth is not None self._auth.async_invalidate_access_token() - async def async_get_access_token(self): + async def async_get_access_token(self) -> str | None: """Get an access token.""" + assert self._auth is not None return await self._auth.async_get_access_token() - async def async_accept_grant(self, code): + async def async_accept_grant(self, code: str) -> str | None: """Accept a grant.""" + assert self._auth is not None return await self._auth.async_do_auth(code) @@ -124,20 +134,20 @@ class SmartHomeView(HomeAssistantView): url = SMART_HOME_HTTP_ENDPOINT name = "api:alexa:smart_home" - def __init__(self, smart_home_config): + def __init__(self, smart_home_config: AlexaConfig) -> None: """Initialize.""" self.smart_home_config = smart_home_config - async def post(self, request): + async def post(self, request: HomeAssistantRequest) -> web.Response | bytes: """Handle Alexa Smart Home requests. The Smart Home API requires the endpoint to be implemented in AWS Lambda, which will need to forward the requests to here and pass back the response. """ - hass = request.app["hass"] - user = request["hass_user"] - message = await request.json() + hass: HomeAssistant = request.app["hass"] + user: User = request["hass_user"] + message: dict[str, Any] = await request.json() _LOGGER.debug("Received Alexa Smart Home request: %s", message) @@ -148,7 +158,13 @@ class SmartHomeView(HomeAssistantView): return b"" if response is None else self.json(response) -async def async_handle_message(hass, config, request, context=None, enabled=True): +async def async_handle_message( + hass: HomeAssistant, + config: AbstractConfig, + request: dict[str, Any], + context: Context | None = None, + enabled: bool = True, +) -> dict[str, Any]: """Handle incoming API messages. If enabled is False, the response to all messages will be a @@ -185,7 +201,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True response = directive.error() except AlexaError as err: response = directive.error( - error_type=err.error_type, + error_type=str(err.error_type), error_message=err.error_message, payload=err.payload, ) @@ -198,9 +214,13 @@ async def async_handle_message(hass, config, request, context=None, enabled=True ) response = directive.error(error_message="Unknown error") - request_info = {"namespace": directive.namespace, "name": directive.name} + request_info: dict[str, Any] = { + "namespace": directive.namespace, + "name": directive.name, + } if directive.has_endpoint: + assert directive.entity_id is not None request_info["entity_id"] = directive.entity_id hass.bus.async_fire( diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 236635a0bb8..7bd80000ca4 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -230,7 +230,7 @@ class CloudClient(Interface): """Process cloud alexa message to client.""" cloud_user = await self._prefs.get_cloud_user() aconfig = await self.get_alexa_config() - return await alexa_smart_home.async_handle_message( # type: ignore[no-any-return, no-untyped-call] + return await alexa_smart_home.async_handle_message( self._hass, aconfig, payload, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index d24ece9b48c..0080c9b02b8 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2798,20 +2798,13 @@ async def test_disabled(hass: HomeAssistant) -> None: hass.states.async_set("switch.test", "on", {"friendly_name": "Test switch"}) request = get_new_request("Alexa.PowerController", "TurnOn", "switch#test") - call_switch = async_mock_service(hass, "switch", "turn_on") + async_mock_service(hass, "switch", "turn_on") - msg = await smart_home.async_handle_message( - hass, get_default_config(hass), request, enabled=False - ) - await hass.async_block_till_done() - - assert "event" in msg - msg = msg["event"] - - assert not call_switch - assert msg["header"]["name"] == "ErrorResponse" - assert msg["header"]["namespace"] == "Alexa" - assert msg["payload"]["type"] == "BRIDGE_UNREACHABLE" + with pytest.raises(AssertionError): + await smart_home.async_handle_message( + hass, get_default_config(hass), request, enabled=False + ) + await hass.async_block_till_done() async def test_endpoint_good_health(hass: HomeAssistant) -> None: From 55619e7d6d95854d5a8463505a41db814c37337f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 12:06:24 +0200 Subject: [PATCH 043/179] Modernize ecobee weather (#98023) --- homeassistant/components/ecobee/weather.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index d38bc82c6f2..359f9ff485c 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -12,7 +12,9 @@ from homeassistant.components.weather import ( ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -59,6 +61,7 @@ class EcobeeWeather(WeatherEntity): _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_has_entity_name = True _attr_name = None + _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" @@ -161,13 +164,12 @@ class EcobeeWeather(WeatherEntity): time = self.weather.get("timestamp", "UNKNOWN") return f"Ecobee weather provided by {station} at {time} UTC" - @property - def forecast(self): + def _forecast(self) -> list[Forecast] | None: """Return the forecast array.""" if "forecasts" not in self.weather: return None - forecasts = [] + forecasts: list[Forecast] = [] date = dt_util.utcnow() for day in range(0, 5): forecast = _process_forecast(self.weather["forecasts"][day]) @@ -181,6 +183,15 @@ class EcobeeWeather(WeatherEntity): return forecasts return None + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast array.""" + return self._forecast() + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self._forecast() + async def async_update(self) -> None: """Get the latest weather data.""" await self.data.update() From 40e256847c5b518fcb2e7f06a52c3717ea7a2a88 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Aug 2023 12:11:52 +0200 Subject: [PATCH 044/179] Add is_admin checks to scene/script/automation APIs (#98025) --- homeassistant/components/config/__init__.py | 5 ++- tests/components/config/test_automation.py | 32 +++++++++++++++ tests/components/config/test_scene.py | 44 +++++++++++++++++++++ tests/components/config/test_script.py | 33 ++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 514154137e4..84a1c2eaa17 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -7,7 +7,7 @@ import os import voluptuous as vol from homeassistant.components import frontend -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, require_admin from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -103,6 +103,7 @@ class BaseEditConfigView(HomeAssistantView): """Delete value.""" raise NotImplementedError + @require_admin async def get(self, request, config_key): """Fetch device specific config.""" hass = request.app["hass"] @@ -115,6 +116,7 @@ class BaseEditConfigView(HomeAssistantView): return self.json(value) + @require_admin async def post(self, request, config_key): """Validate config and return results.""" try: @@ -156,6 +158,7 @@ class BaseEditConfigView(HomeAssistantView): return self.json({"result": "ok"}) + @require_admin async def delete(self, request, config_key): """Remove an entry.""" hass = request.app["hass"] diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 11f17199e5a..abe0ed90e86 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -372,3 +372,35 @@ async def test_delete_automation( assert hass_config_store["automations.yaml"] == [{"id": "moon"}] assert len(ent_reg.entities) == 1 + + +@pytest.mark.parametrize("automation_config", ({},)) +async def test_api_calls_require_admin( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_read_only_access_token: str, + hass_config_store, + setup_automation, +) -> None: + """Test cloud APIs endpoints do not work as a normal user.""" + with patch.object(config, "SECTIONS", ["automation"]): + await async_setup_component(hass, "config", {}) + + hass_config_store["automations.yaml"] = [{"id": "sun"}, {"id": "moon"}] + + client = await hass_client(hass_read_only_access_token) + + # Get + resp = await client.get("/api/config/automation/config/moon") + assert resp.status == HTTPStatus.UNAUTHORIZED + + # Update + resp = await client.post( + "/api/config/automation/config/moon", + data=json.dumps({"trigger": [], "action": [], "condition": []}), + ) + assert resp.status == HTTPStatus.UNAUTHORIZED + + # Delete + resp = await client.delete("/api/config/automation/config/sun") + assert resp.status == HTTPStatus.UNAUTHORIZED diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index d07db81b715..1f09d5e9989 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -221,3 +221,47 @@ async def test_delete_scene( ] assert len(ent_reg.entities) == 1 + + +@pytest.mark.parametrize("scene_config", ({},)) +async def test_api_calls_require_admin( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_read_only_access_token: str, + hass_config_store, + setup_scene, +) -> None: + """Test scene APIs endpoints do not work as a normal user.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + hass_config_store["scenes.yaml"] = [ + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ] + + client = await hass_client(hass_read_only_access_token) + + # Get + resp = await client.get("/api/config/scene/config/light_off") + assert resp.status == HTTPStatus.UNAUTHORIZED + + # Update + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + assert resp.status == HTTPStatus.UNAUTHORIZED + + # Delete + resp = await client.delete("/api/config/scene/config/light_on") + assert resp.status == HTTPStatus.UNAUTHORIZED diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 86ea2cf9e7f..cc0352301b4 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -314,3 +314,36 @@ async def test_delete_script( assert hass_config_store["scripts.yaml"] == {"one": {}} assert len(ent_reg.entities) == 1 + + +@pytest.mark.parametrize("script_config", ({},)) +async def test_api_calls_require_admin( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_read_only_access_token: str, + hass_config_store, +) -> None: + """Test script APIs endpoints do not work as a normal user.""" + with patch.object(config, "SECTIONS", ["script"]): + await async_setup_component(hass, "config", {}) + + hass_config_store["scripts.yaml"] = { + "moon": {"alias": "Moon"}, + } + + client = await hass_client(hass_read_only_access_token) + + # Get + resp = await client.get("/api/config/script/config/moon") + assert resp.status == HTTPStatus.UNAUTHORIZED + + # Update + resp = await client.post( + "/api/config/script/config/moon", + data=json.dumps({"sequence": []}), + ) + assert resp.status == HTTPStatus.UNAUTHORIZED + + # Delete + resp = await client.delete("/api/config/script/config/moon") + assert resp.status == HTTPStatus.UNAUTHORIZED From f6273bfca5ae0f77bac1d611a87f5f834d343372 Mon Sep 17 00:00:00 2001 From: Jadson Santos <42282908+gtjadsonsantos@users.noreply.github.com> Date: Tue, 8 Aug 2023 07:15:08 -0300 Subject: [PATCH 045/179] Add prometheus requires_auth parameter (#92964) Co-authored-by: Erik Montnemery --- homeassistant/components/prometheus/__init__.py | 9 +++++++-- tests/components/prometheus/test_init.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2bc9fbb5324..e5d7f6cb060 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -52,6 +52,7 @@ API_ENDPOINT = "/api/prometheus" DOMAIN = "prometheus" CONF_FILTER = "filter" +CONF_REQUIRES_AUTH = "requires_auth" CONF_PROM_NAMESPACE = "namespace" CONF_COMPONENT_CONFIG = "component_config" CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" @@ -70,6 +71,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, vol.Optional(CONF_PROM_NAMESPACE, default=DEFAULT_NAMESPACE): cv.string, + vol.Optional(CONF_REQUIRES_AUTH, default=True): cv.boolean, vol.Optional(CONF_DEFAULT_METRIC): cv.string, vol.Optional(CONF_OVERRIDE_METRIC): cv.string, vol.Optional(CONF_COMPONENT_CONFIG, default={}): vol.Schema( @@ -90,7 +92,9 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Activate Prometheus component.""" - hass.http.register_view(PrometheusView(prometheus_client)) + hass.http.register_view( + PrometheusView(prometheus_client, config[DOMAIN][CONF_REQUIRES_AUTH]) + ) conf = config[DOMAIN] entity_filter = conf[CONF_FILTER] @@ -650,8 +654,9 @@ class PrometheusView(HomeAssistantView): url = API_ENDPOINT name = "api:prometheus" - def __init__(self, prometheus_cli): + def __init__(self, prometheus_cli, requires_auth: bool) -> None: """Initialize Prometheus view.""" + self.requires_auth = requires_auth self.prometheus_cli = prometheus_cli async def get(self, request): diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 8b0acb9c5b0..09c8a37dc2a 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1581,6 +1581,7 @@ async def test_full_config(hass: HomeAssistant, mock_client) -> None: "namespace": "ns", "default_metric": "m", "override_metric": "m", + "requires_auth": False, "component_config": {"fake.test": {"override_metric": "km"}}, "component_config_glob": {"fake.time_*": {"override_metric": "h"}}, "component_config_domain": {"climate": {"override_metric": "°C"}}, From 50ef6749023b2d6b89b887ae904aa81f62da2142 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Aug 2023 12:19:20 +0200 Subject: [PATCH 046/179] Use require_admin decorator for check_config permissions (#98028) --- homeassistant/components/config/core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 5a825e5676a..9771e12f1d6 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -5,11 +5,10 @@ from typing import Any import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, require_admin from homeassistant.components.sensor import async_update_suggested_units from homeassistant.config import async_check_ha_config_file from homeassistant.core import HomeAssistant -from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import location, unit_system @@ -29,11 +28,9 @@ class CheckConfigView(HomeAssistantView): url = "/api/config/core/check_config" name = "api:config:core:check_config" + @require_admin async def post(self, request): """Validate configuration and return results.""" - if not request["hass_user"].is_admin: - raise Unauthorized() - errors = await async_check_ha_config_file(request.app["hass"]) state = "invalid" if errors else "valid" From 0d55a7600e3ce2e4fd29727e2d0271bcc28fdb44 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 14:21:52 +0200 Subject: [PATCH 047/179] Modernize met_eireann weather (#98030) --- homeassistant/components/met_eireann/const.py | 4 - .../components/met_eireann/weather.py | 63 ++++++-- .../met_eireann/snapshots/test_weather.ambr | 89 +++++++++++ tests/components/met_eireann/test_weather.py | 144 +++++++++++++++++- 4 files changed, 277 insertions(+), 23 deletions(-) create mode 100644 tests/components/met_eireann/snapshots/test_weather.ambr diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 1cab9c9099f..9316aad1b17 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -9,13 +9,11 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, ) @@ -29,12 +27,10 @@ HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" FORECAST_MAP = { - ATTR_FORECAST_CONDITION: "condition", ATTR_FORECAST_NATIVE_PRESSURE: "pressure", ATTR_FORECAST_PRECIPITATION: "precipitation", ATTR_FORECAST_NATIVE_TEMP: "temperature", ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", - ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index bf0d7214c6e..67c0a830c61 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -1,10 +1,13 @@ """Support for Met Éireann weather service.""" import logging +from typing import cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TIME, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -16,7 +19,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -28,7 +31,7 @@ from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP _LOGGER = logging.getLogger(__name__) -def format_condition(condition: str): +def format_condition(condition: str | None) -> str | None: """Map the conditions provided by the weather API to those supported by the frontend.""" if condition is not None: for key, value in CONDITION_MAP.items(): @@ -60,6 +63,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" @@ -67,6 +73,15 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): self._config = config self._hourly = hourly + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + super()._handle_coordinator_update() + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + self.hass, self.async_update_listeners(("daily", "hourly")) + ) + @property def unique_id(self): """Return unique ID.""" @@ -126,35 +141,51 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Return the wind direction.""" return self.coordinator.data.current_weather_data.get("wind_bearing") - @property - def forecast(self): + def _forecast(self, hourly: bool) -> list[Forecast]: """Return the forecast array.""" - if self._hourly: + if hourly: me_forecast = self.coordinator.data.hourly_forecast else: me_forecast = self.coordinator.data.daily_forecast required_keys = {"temperature", "datetime"} - ha_forecast = [] + ha_forecast: list[Forecast] = [] for item in me_forecast: if not set(item).issuperset(required_keys): continue - ha_item = { - k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None - } - if ha_item.get(ATTR_FORECAST_CONDITION): - ha_item[ATTR_FORECAST_CONDITION] = format_condition( - ha_item[ATTR_FORECAST_CONDITION] - ) - # Convert timestamp to UTC - if ha_item.get(ATTR_FORECAST_TIME): + ha_item: Forecast = cast( + Forecast, + { + k: item[v] + for k, v in FORECAST_MAP.items() + if item.get(v) is not None + }, + ) + # Convert condition + if item.get("condition"): + ha_item[ATTR_FORECAST_CONDITION] = format_condition(item["condition"]) + # Convert timestamp to UTC string + if item.get("datetime"): ha_item[ATTR_FORECAST_TIME] = dt_util.as_utc( - ha_item.get(ATTR_FORECAST_TIME) + item["datetime"] ).isoformat() ha_forecast.append(ha_item) return ha_forecast + @property + def forecast(self) -> list[Forecast]: + """Return the forecast array.""" + return self._forecast(self._hourly) + + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + return self._forecast(False) + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast in native units.""" + return self._forecast(True) + @property def device_info(self): """Device info.""" diff --git a/tests/components/met_eireann/snapshots/test_weather.ambr b/tests/components/met_eireann/snapshots/test_weather.ambr new file mode 100644 index 00000000000..81d7a52aa06 --- /dev/null +++ b/tests/components/met_eireann/snapshots/test_weather.ambr @@ -0,0 +1,89 @@ +# serializer version: 1 +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-08T12:00:00+00:00', + 'temperature': 10.0, + }), + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-09T12:00:00+00:00', + 'temperature': 20.0, + }), + ]), + }) +# --- +# name: test_forecast_service.1 + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-08T12:00:00+00:00', + 'temperature': 10.0, + }), + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-09T12:00:00+00:00', + 'temperature': 20.0, + }), + ]), + }) +# --- +# name: test_forecast_subscription[daily] + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-08T12:00:00+00:00', + 'temperature': 10.0, + }), + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-09T12:00:00+00:00', + 'temperature': 20.0, + }), + ]) +# --- +# name: test_forecast_subscription[daily].1 + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-08T12:00:00+00:00', + 'temperature': 15.0, + }), + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-09T12:00:00+00:00', + 'temperature': 25.0, + }), + ]) +# --- +# name: test_forecast_subscription[hourly] + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-08T12:00:00+00:00', + 'temperature': 10.0, + }), + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-09T12:00:00+00:00', + 'temperature': 20.0, + }), + ]) +# --- +# name: test_forecast_subscription[hourly].1 + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-08T12:00:00+00:00', + 'temperature': 15.0, + }), + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2023-08-09T12:00:00+00:00', + 'temperature': 25.0, + }), + ]) +# --- diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py index 6983a47ff4b..e14cd485cc6 100644 --- a/tests/components/met_eireann/test_weather.py +++ b/tests/components/met_eireann/test_weather.py @@ -1,13 +1,25 @@ """Test Met Éireann weather entity.""" +import datetime + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.met_eireann import UPDATE_INTERVAL from homeassistant.components.met_eireann.const import DOMAIN +from homeassistant.components.weather import ( + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator -async def test_weather(hass: HomeAssistant, mock_weather) -> None: - """Test weather entity.""" - # Create a mock configuration for testing +async def setup_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create a mock configuration for testing.""" mock_data = MockConfigEntry( domain=DOMAIN, data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, @@ -16,6 +28,12 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None: await hass.config_entries.async_setup(mock_data.entry_id) await hass.async_block_till_done() + return mock_data + + +async def test_weather(hass: HomeAssistant, mock_weather) -> None: + """Test weather entity.""" + await setup_config_entry(hass) assert len(hass.states.async_entity_ids("weather")) == 1 assert len(mock_weather.mock_calls) == 4 @@ -29,3 +47,123 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None: await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 0 + + +async def test_forecast_service( + hass: HomeAssistant, + mock_weather, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + mock_weather.get_forecast.return_value = [ + { + "condition": "SleetSunThunder", + "datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC), + "temperature": 10.0, + }, + { + "condition": "SleetSunThunder", + "datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC), + "temperature": 20.0, + }, + ] + + await setup_config_entry(hass) + assert len(hass.states.async_entity_ids("weather")) == 1 + entity_id = hass.states.async_entity_ids("weather")[0] + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity_id, + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity_id, + "type": "hourly", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + +@pytest.mark.parametrize("forecast_type", ["daily", "hourly"]) +async def test_forecast_subscription( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + mock_weather, + snapshot: SnapshotAssertion, + forecast_type: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + + mock_weather.get_forecast.return_value = [ + { + "condition": "SleetSunThunder", + "datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC), + "temperature": 10.0, + }, + { + "condition": "SleetSunThunder", + "datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC), + "temperature": 20.0, + }, + ] + + await setup_config_entry(hass) + assert len(hass.states.async_entity_ids("weather")) == 1 + entity_id = hass.states.async_entity_ids("weather")[0] + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": entity_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert forecast1 == snapshot + + mock_weather.get_forecast.return_value = [ + { + "condition": "SleetSunThunder", + "datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC), + "temperature": 15.0, + }, + { + "condition": "SleetSunThunder", + "datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC), + "temperature": 25.0, + }, + ] + + freezer.tick(UPDATE_INTERVAL + datetime.timedelta(seconds=1)) + await hass.async_block_till_done() + msg = await client.receive_json() + + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast2 = msg["event"]["forecast"] + + assert forecast2 == snapshot From 8b56e28838e21888350c9f27b3d707619bb9f7f9 Mon Sep 17 00:00:00 2001 From: Massimiliano Cannarozzo Date: Tue, 8 Aug 2023 14:35:41 +0200 Subject: [PATCH 048/179] Add neato dismiss alert button (#97572) * Bump pybotvac * Add neato dismiss alert button * fixup! Add neato dismiss alert button * fixup! Add neato dismiss alert button Co-authored-by: Joost Lekkerkerker * fixup! Add neato dismiss alert button Co-authored-by: Joost Lekkerkerker * fixup! Add neato dismiss alert button * fixup! Add neato dismiss alert button * fixup! Add neato dismiss alert button Co-authored-by: G Johansson * fixup! Add neato dismiss alert button Co-authored-by: G Johansson * fixup! Add neato dismiss alert button * fixup! Add neato dismiss alert button --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: G Johansson --- .coveragerc | 1 + homeassistant/components/neato/__init__.py | 8 +++- homeassistant/components/neato/button.py | 42 ++++++++++++++++++++ homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/neato/button.py diff --git a/.coveragerc b/.coveragerc index 01e1d0d3b0e..2e35001ee14 100644 --- a/.coveragerc +++ b/.coveragerc @@ -772,6 +772,7 @@ omit = homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py + homeassistant/components/neato/button.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 4daa7e5b14d..52bc841f3b5 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -40,7 +40,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [Platform.CAMERA, Platform.VACUUM, Platform.SWITCH, Platform.SENSOR] +PLATFORMS = [ + Platform.CAMERA, + Platform.VACUUM, + Platform.SWITCH, + Platform.SENSOR, + Platform.BUTTON, +] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/neato/button.py b/homeassistant/components/neato/button.py new file mode 100644 index 00000000000..f215bbe7225 --- /dev/null +++ b/homeassistant/components/neato/button.py @@ -0,0 +1,42 @@ +"""Support for Neato buttons.""" +from __future__ import annotations + +from pybotvac import Robot + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import NEATO_DOMAIN, NEATO_ROBOTS + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Neato button from config entry.""" + entities = [NeatoDismissAlertButton(robot) for robot in hass.data[NEATO_ROBOTS]] + + async_add_entities(entities, True) + + +class NeatoDismissAlertButton(ButtonEntity): + """Representation of a dismiss_alert button entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + robot: Robot, + ) -> None: + """Initialize a dismiss_alert Neato button entity.""" + self.robot = robot + self._attr_name = f"{robot.name} Dismiss Alert" + self._attr_unique_id = f"{robot.serial}_dismiss_alert" + self._attr_device_info = DeviceInfo(identifiers={(NEATO_DOMAIN, robot.serial)}) + + async def async_press(self) -> None: + """Press the button.""" + await self.hass.async_add_executor_job(self.robot.dismiss_current_alert) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 654a57ab2bb..5222ec938c8 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/neato", "iot_class": "cloud_polling", "loggers": ["pybotvac"], - "requirements": ["pybotvac==0.0.23"] + "requirements": ["pybotvac==0.0.24"] } diff --git a/requirements_all.txt b/requirements_all.txt index 30281d1accd..1aac3651bc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1584,7 +1584,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.neato -pybotvac==0.0.23 +pybotvac==0.0.24 # homeassistant.components.braviatv pybravia==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 880e6e15684..9736a8d176a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1187,7 +1187,7 @@ pybalboa==1.0.1 pyblackbird==0.6 # homeassistant.components.neato -pybotvac==0.0.23 +pybotvac==0.0.24 # homeassistant.components.braviatv pybravia==0.3.3 From 2a48159b6970aa7075dcc5a0a4acb5f84a589786 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Aug 2023 15:46:54 +0200 Subject: [PATCH 049/179] Alexa typing part 6 (state_report) (#97920) state_report --- .../components/alexa/state_report.py | 132 +++++++++++------- .../components/cloud/alexa_config.py | 7 +- 2 files changed, 85 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index ecec1451497..4e3c33386ca 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -5,7 +5,8 @@ import asyncio from http import HTTPStatus import json import logging -from typing import TYPE_CHECKING, cast +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 import aiohttp @@ -46,17 +47,22 @@ DEFAULT_TIMEOUT = 10 class AlexaDirective: """An incoming Alexa directive.""" - def __init__(self, request): + entity: State + entity_id: str | None + endpoint: AlexaEntity + instance: str | None + + def __init__(self, request: dict[str, Any]) -> None: """Initialize a directive.""" - self._directive = request[API_DIRECTIVE] - self.namespace = self._directive[API_HEADER]["namespace"] - self.name = self._directive[API_HEADER]["name"] - self.payload = self._directive[API_PAYLOAD] - self.has_endpoint = API_ENDPOINT in self._directive + self._directive: dict[str, Any] = request[API_DIRECTIVE] + self.namespace: str = self._directive[API_HEADER]["namespace"] + self.name: str = self._directive[API_HEADER]["name"] + self.payload: dict[str, Any] = self._directive[API_PAYLOAD] + self.has_endpoint: bool = API_ENDPOINT in self._directive + self.instance = None + self.entity_id = None - self.entity = self.entity_id = self.endpoint = self.instance = None - - def load_entity(self, hass, config): + def load_entity(self, hass: HomeAssistant, config: AbstractConfig) -> None: """Set attributes related to the entity for this request. Sets these attributes when self.has_endpoint is True: @@ -71,18 +77,24 @@ class AlexaDirective: Will raise AlexaInvalidEndpointError if the endpoint in the request is malformed or nonexistent. """ - _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] + _endpoint_id: str = self._directive[API_ENDPOINT]["endpointId"] self.entity_id = _endpoint_id.replace("#", ".") - self.entity = hass.states.get(self.entity_id) - if not self.entity or not config.should_expose(self.entity_id): + entity: State | None = hass.states.get(self.entity_id) + if not entity or not config.should_expose(self.entity_id): raise AlexaInvalidEndpointError(_endpoint_id) + self.entity = entity self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) if "instance" in self._directive[API_HEADER]: self.instance = self._directive[API_HEADER]["instance"] - def response(self, name="Response", namespace="Alexa", payload=None): + def response( + self, + name: str = "Response", + namespace: str = "Alexa", + payload: dict[str, Any] | None = None, + ) -> AlexaResponse: """Create an API formatted response. Async friendly. @@ -100,11 +112,11 @@ class AlexaDirective: def error( self, - namespace="Alexa", - error_type="INTERNAL_ERROR", - error_message="", - payload=None, - ): + namespace: str = "Alexa", + error_type: str = "INTERNAL_ERROR", + error_message: str = "", + payload: dict[str, Any] | None = None, + ) -> AlexaResponse: """Create a API formatted error response. Async friendly. @@ -127,10 +139,12 @@ class AlexaDirective: class AlexaResponse: """Class to hold a response.""" - def __init__(self, name, namespace, payload=None): + def __init__( + self, name: str, namespace: str, payload: dict[str, Any] | None = None + ) -> None: """Initialize the response.""" payload = payload or {} - self._response = { + self._response: dict[str, Any] = { API_EVENT: { API_HEADER: { "namespace": namespace, @@ -143,16 +157,16 @@ class AlexaResponse: } @property - def name(self): + def name(self) -> str: """Return the name of this response.""" return self._response[API_EVENT][API_HEADER]["name"] @property - def namespace(self): + def namespace(self) -> str: """Return the namespace of this response.""" return self._response[API_EVENT][API_HEADER]["namespace"] - def set_correlation_token(self, token): + def set_correlation_token(self, token: str) -> None: """Set the correlationToken. This should normally mirror the value from a request, and is set by @@ -160,7 +174,9 @@ class AlexaResponse: """ self._response[API_EVENT][API_HEADER]["correlationToken"] = token - def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): + def set_endpoint_full( + self, bearer_token: str | None, endpoint_id: str | None + ) -> None: """Set the endpoint dictionary. This is used to send proactive messages to Alexa. @@ -172,10 +188,7 @@ class AlexaResponse: if endpoint_id is not None: self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id - if cookie is not None: - self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie - - def set_endpoint(self, endpoint): + def set_endpoint(self, endpoint: dict[str, Any]) -> None: """Set the endpoint. This should normally mirror the value from a request, and is set by @@ -187,7 +200,7 @@ class AlexaResponse: context = self._response.setdefault(API_CONTEXT, {}) return context.setdefault("properties", []) - def add_context_property(self, prop): + def add_context_property(self, prop: dict[str, Any]) -> None: """Add a property to the response context. The Alexa response includes a list of properties which provides @@ -204,7 +217,7 @@ class AlexaResponse: """ self._properties().append(prop) - def merge_context_properties(self, endpoint): + def merge_context_properties(self, endpoint: AlexaEntity) -> None: """Add all properties from given endpoint if not already set. Handlers should be using .add_context_property(). @@ -216,12 +229,14 @@ class AlexaResponse: if (prop["namespace"], prop["name"]) not in already_set: self.add_context_property(prop) - def serialize(self): + def serialize(self) -> dict[str, Any]: """Return response as a JSON-able data structure.""" return self._response -async def async_enable_proactive_mode(hass, smart_home_config): +async def async_enable_proactive_mode( + hass: HomeAssistant, smart_home_config: AbstractConfig +): """Enable the proactive mode. Proactive mode makes this component report state changes to Alexa. @@ -233,12 +248,12 @@ async def async_enable_proactive_mode(hass, smart_home_config): def extra_significant_check( hass: HomeAssistant, old_state: str, - old_attrs: dict, - old_extra_arg: dict, + old_attrs: dict[Any, Any] | MappingProxyType[Any, Any], + old_extra_arg: Any, new_state: str, - new_attrs: dict, - new_extra_arg: dict, - ): + new_attrs: dict[str, Any] | MappingProxyType[Any, Any], + new_extra_arg: Any, + ) -> bool: """Check if the serialized data has changed.""" return old_extra_arg is not None and old_extra_arg != new_extra_arg @@ -248,7 +263,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): changed_entity: str, old_state: State | None, new_state: State | None, - ): + ) -> None: if not hass.is_running: return @@ -307,8 +322,13 @@ async def async_enable_proactive_mode(hass, smart_home_config): async def async_send_changereport_message( - hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True -): + hass: HomeAssistant, + config: AbstractConfig, + alexa_entity: AlexaEntity, + alexa_properties: list[dict[str, Any]], + *, + invalidate_access_token: bool = True, +) -> None: """Send a ChangeReport message for an Alexa entity. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events @@ -322,11 +342,11 @@ async def async_send_changereport_message( ) return - headers = {"Authorization": f"Bearer {token}"} + headers: dict[str, Any] = {"Authorization": f"Bearer {token}"} endpoint = alexa_entity.alexa_id() - payload = { + payload: dict[str, Any] = { API_CHANGE: { "cause": {"type": Cause.APP_INTERACTION}, "properties": alexa_properties, @@ -339,6 +359,7 @@ async def async_send_changereport_message( message_serialized = message.serialize() session = async_get_clientsession(hass) + assert config.endpoint is not None try: async with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.post( @@ -393,9 +414,9 @@ async def async_send_add_or_update_message( """ token = await config.async_get_access_token() - headers = {"Authorization": f"Bearer {token}"} + headers: dict[str, Any] = {"Authorization": f"Bearer {token}"} - endpoints = [] + endpoints: list[dict[str, Any]] = [] for entity_id in entity_ids: if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS: @@ -407,7 +428,10 @@ async def async_send_add_or_update_message( alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state) endpoints.append(alexa_entity.serialize_discovery()) - payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} + payload: dict[str, Any] = { + "endpoints": endpoints, + "scope": {"type": "BearerToken", "token": token}, + } message = AlexaResponse( name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload @@ -431,9 +455,9 @@ async def async_send_delete_message( """ token = await config.async_get_access_token() - headers = {"Authorization": f"Bearer {token}"} + headers: dict[str, Any] = {"Authorization": f"Bearer {token}"} - endpoints = [] + endpoints: list[dict[str, Any]] = [] for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] @@ -443,7 +467,10 @@ async def async_send_delete_message( endpoints.append({"endpointId": generate_alexa_id(entity_id)}) - payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} + payload: dict[str, Any] = { + "endpoints": endpoints, + "scope": {"type": "BearerToken", "token": token}, + } message = AlexaResponse( name="DeleteReport", namespace="Alexa.Discovery", payload=payload @@ -458,14 +485,16 @@ async def async_send_delete_message( ) -async def async_send_doorbell_event_message(hass, config, alexa_entity): +async def async_send_doorbell_event_message( + hass: HomeAssistant, config: AbstractConfig, alexa_entity: AlexaEntity +) -> None: """Send a DoorbellPress event message for an Alexa entity. https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html """ token = await config.async_get_access_token() - headers = {"Authorization": f"Bearer {token}"} + headers: dict[str, Any] = {"Authorization": f"Bearer {token}"} endpoint = alexa_entity.alexa_id() @@ -483,6 +512,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity): message_serialized = message.serialize() session = async_get_clientsession(hass) + assert config.endpoint is not None try: async with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.post( diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 8c1300f6228..3ceb02972d1 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any import aiohttp import async_timeout from hass_nabucasa import Cloud, cloud_api +from yarl import URL from homeassistant.components import persistent_notification from homeassistant.components.alexa import ( @@ -149,7 +150,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): self._token_valid: datetime | None = None self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA) self._alexa_sync_unsub: Callable[[], None] | None = None - self._endpoint: Any = None + self._endpoint: str | URL | None = None @property def enabled(self) -> bool: @@ -175,7 +176,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): ) @property - def endpoint(self) -> Any | None: + def endpoint(self) -> str | URL | None: """Endpoint for report state.""" if self._endpoint is None: raise ValueError("No endpoint available. Fetch access token first") @@ -309,7 +310,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): """Invalidate access token.""" self._token_valid = None - async def async_get_access_token(self) -> Any: + async def async_get_access_token(self) -> str | None: """Get an access token.""" if self._token_valid is not None and self._token_valid > utcnow(): return self._token From a224b668d7deee92a470ee82b5d4f24f6c2cb430 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 8 Aug 2023 16:09:53 +0200 Subject: [PATCH 050/179] modbus: Adjust read count by slave_count (#97908) --- homeassistant/components/modbus/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 1edd732efeb..bed5932a303 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -49,7 +49,7 @@ async def async_setup_platform( hub = get_hub(hass, discovery_info[CONF_NAME]) for entry in discovery_info[CONF_SENSORS]: slave_count = entry.get(CONF_SLAVE_COUNT, 0) - sensor = ModbusRegisterSensor(hub, entry) + sensor = ModbusRegisterSensor(hub, entry, slave_count) if slave_count > 0: sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry)) sensors.append(sensor) @@ -63,9 +63,12 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): self, hub: ModbusHub, entry: dict[str, Any], + slave_count: int, ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) + if slave_count: + self._count = self._count * slave_count self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) From 4a4523c249e8d2767bcd9429337fdcb886239592 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 8 Aug 2023 16:38:37 +0200 Subject: [PATCH 051/179] Bump plugwise to v0.31.9 (#97203) Co-authored-by: Franck Nijhof Co-authored-by: Joost Lekkerkerker Co-authored-by: Bouwe --- .../components/plugwise/binary_sensor.py | 26 +- homeassistant/components/plugwise/climate.py | 16 +- .../components/plugwise/coordinator.py | 18 +- homeassistant/components/plugwise/entity.py | 2 +- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 35 +- homeassistant/components/plugwise/select.py | 32 +- homeassistant/components/plugwise/sensor.py | 135 ++-- homeassistant/components/plugwise/switch.py | 21 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plugwise/conftest.py | 80 ++- .../plugwise/fixtures/adam_jip/all_data.json | 266 +++++++ .../{p1v4_3ph => adam_jip}/notifications.json | 0 .../all_data.json | 650 +++++++++--------- .../anna_heatpump_heating/all_data.json | 139 ++-- .../fixtures/m_adam_cooling/all_data.json | 196 +++--- .../fixtures/m_adam_heating/all_data.json | 195 +++--- .../m_anna_heatpump_cooling/all_data.json | 141 ++-- .../m_anna_heatpump_idle/all_data.json | 140 ++-- .../fixtures/p1v3_full_option/all_data.json | 50 +- .../all_data.json | 64 +- .../p1v4_442_triple/notifications.json | 1 + .../fixtures/stretch_v31/all_data.json | 146 ++-- .../components/plugwise/test_binary_sensor.py | 9 +- tests/components/plugwise/test_climate.py | 18 +- tests/components/plugwise/test_diagnostics.py | 13 +- tests/components/plugwise/test_init.py | 4 +- tests/components/plugwise/test_sensor.py | 58 +- tests/components/plugwise/test_switch.py | 6 +- 30 files changed, 1415 insertions(+), 1052 deletions(-) create mode 100644 tests/components/plugwise/fixtures/adam_jip/all_data.json rename tests/components/plugwise/fixtures/{p1v4_3ph => adam_jip}/notifications.json (100%) rename tests/components/plugwise/fixtures/{p1v4_3ph => p1v4_442_triple}/all_data.json (88%) create mode 100644 tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 956dd7f36da..5da82ab4105 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -1,11 +1,11 @@ """Plugwise Binary Sensor component for Home Assistant.""" from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Mapping from dataclasses import dataclass from typing import Any -from plugwise import SmileBinarySensors +from plugwise.constants import BinarySensorType from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -24,18 +24,10 @@ SEVERITIES = ["other", "info", "warning", "error"] @dataclass -class PlugwiseBinarySensorMixin: - """Mixin for required Plugwise binary sensor base description keys.""" - - value_fn: Callable[[SmileBinarySensors], bool] - - -@dataclass -class PlugwiseBinarySensorEntityDescription( - BinarySensorEntityDescription, PlugwiseBinarySensorMixin -): +class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes a Plugwise binary sensor entity.""" + key: BinarySensorType icon_off: str | None = None @@ -46,14 +38,12 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:hvac", icon_off="mdi:hvac-off", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["compressor_state"], ), PlugwiseBinarySensorEntityDescription( key="cooling_enabled", translation_key="cooling_enabled", icon="mdi:snowflake-thermometer", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["cooling_enabled"], ), PlugwiseBinarySensorEntityDescription( key="dhw_state", @@ -61,7 +51,6 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:water-pump", icon_off="mdi:water-pump-off", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["dhw_state"], ), PlugwiseBinarySensorEntityDescription( key="flame_state", @@ -70,7 +59,6 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:fire", icon_off="mdi:fire-off", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["flame_state"], ), PlugwiseBinarySensorEntityDescription( key="heating_state", @@ -78,7 +66,6 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:radiator", icon_off="mdi:radiator-off", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["heating_state"], ), PlugwiseBinarySensorEntityDescription( key="cooling_state", @@ -86,7 +73,6 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:snowflake", icon_off="mdi:snowflake-off", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["cooling_state"], ), PlugwiseBinarySensorEntityDescription( key="slave_boiler_state", @@ -94,7 +80,6 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:fire", icon_off="mdi:circle-off-outline", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["slave_boiler_state"], ), PlugwiseBinarySensorEntityDescription( key="plugwise_notification", @@ -102,7 +87,6 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon="mdi:mailbox-up-outline", icon_off="mdi:mailbox-outline", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data["plugwise_notification"], ), ) @@ -154,7 +138,7 @@ class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.entity_description.value_fn(self.device["binary_sensors"]) + return self.device["binary_sensors"][self.entity_description.key] @property def icon(self) -> str | None: diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index d0a65799807..5be09a062e2 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -130,13 +130,13 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): if control_state == "off": return HVACAction.IDLE - hc_data = self.coordinator.data.devices[ - self.coordinator.data.gateway["heater_id"] - ] - if hc_data["binary_sensors"]["heating_state"]: - return HVACAction.HEATING - if hc_data["binary_sensors"].get("cooling_state"): - return HVACAction.COOLING + heater: str | None = self.coordinator.data.gateway["heater_id"] + if heater: + heater_data = self.coordinator.data.devices[heater] + if heater_data["binary_sensors"]["heating_state"]: + return HVACAction.HEATING + if heater_data["binary_sensors"].get("cooling_state"): + return HVACAction.COOLING return HVACAction.IDLE @@ -150,7 +150,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Return entity specific state attributes.""" return { "available_schemas": self.device["available_schedules"], - "selected_schema": self.device["selected_schedule"], + "selected_schema": self.device["select_schedule"], } @plugwise_command diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index afcd673ef7d..395ec4e6e63 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -1,9 +1,7 @@ """DataUpdateCoordinator for Plugwise.""" from datetime import timedelta -from typing import NamedTuple, cast -from plugwise import Smile -from plugwise.constants import DeviceData, GatewayData +from plugwise import PlugwiseData, Smile from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, @@ -23,13 +21,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER -class PlugwiseData(NamedTuple): - """Plugwise data stored in the DataUpdateCoordinator.""" - - gateway: GatewayData - devices: dict[str, DeviceData] - - class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): """Class to manage fetching Plugwise data from single endpoint.""" @@ -65,13 +56,13 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): """Connect to the Plugwise Smile.""" self._connected = await self.api.connect() self.api.get_all_devices() - self.name = self.api.smile_name self.update_interval = DEFAULT_SCAN_INTERVAL.get( str(self.api.smile_type), timedelta(seconds=60) ) async def _async_update_data(self) -> PlugwiseData: """Fetch data from Plugwise.""" + try: if not self._connected: await self._connect() @@ -87,7 +78,4 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): raise ConfigEntryError("Device with unsupported firmware") from err except ConnectionFailedError as err: raise UpdateFailed("Failed to connect to the Plugwise Smile") from err - return PlugwiseData( - gateway=cast(GatewayData, data[0]), - devices=cast(dict[str, DeviceData], data[1]), - ) + return data diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index e2ab5445f07..c0f38cf6d5c 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -67,7 +67,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): """Return if entity is available.""" return ( self._dev_id in self.coordinator.data.devices - and ("available" not in self.device or self.device["available"]) + and ("available" not in self.device or self.device["available"] is True) and super().available ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 4fdcd0a8bdd..ef0f01b38f7 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.31.1"], + "requirements": ["plugwise==0.31.9"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 25667ea16c6..102d94f91b7 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -4,7 +4,8 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass -from plugwise import ActuatorData, Smile +from plugwise import Smile +from plugwise.constants import NumberType from homeassistant.components.number import ( NumberDeviceClass, @@ -27,10 +28,6 @@ class PlugwiseEntityDescriptionMixin: """Mixin values for Plugwise entities.""" command: Callable[[Smile, str, float], Awaitable[None]] - native_max_value_fn: Callable[[ActuatorData], float] - native_min_value_fn: Callable[[ActuatorData], float] - native_step_fn: Callable[[ActuatorData], float] - native_value_fn: Callable[[ActuatorData], float] @dataclass @@ -39,6 +36,8 @@ class PlugwiseNumberEntityDescription( ): """Class describing Plugwise Number entities.""" + key: NumberType + NUMBER_TYPES = ( PlugwiseNumberEntityDescription( @@ -48,10 +47,6 @@ NUMBER_TYPES = ( device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - native_max_value_fn=lambda data: data["upper_bound"], - native_min_value_fn=lambda data: data["lower_bound"], - native_step_fn=lambda data: data["resolution"], - native_value_fn=lambda data: data["setpoint"], ), ) @@ -70,7 +65,7 @@ async def async_setup_entry( entities: list[PlugwiseNumberEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in NUMBER_TYPES: - if (actuator := device.get(description.key)) and "setpoint" in actuator: + if description.key in device: entities.append( PlugwiseNumberEntity(coordinator, device_id, description) ) @@ -91,30 +86,18 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): ) -> None: """Initiate Plugwise Number.""" super().__init__(coordinator, device_id) - self.actuator = self.device[description.key] self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" self._attr_mode = NumberMode.BOX - @property - def native_max_value(self) -> float: - """Return the setpoint max. value.""" - return self.entity_description.native_max_value_fn(self.actuator) - - @property - def native_min_value(self) -> float: - """Return the setpoint min. value.""" - return self.entity_description.native_min_value_fn(self.actuator) - - @property - def native_step(self) -> float: - """Return the setpoint step value.""" - return max(self.entity_description.native_step_fn(self.actuator), 1) + self._attr_native_max_value = self.device[description.key]["upper_bound"] + self._attr_native_min_value = self.device[description.key]["lower_bound"] + self._attr_native_step = max(self.device[description.key]["resolution"], 0.5) @property def native_value(self) -> float: """Return the present setpoint value.""" - return self.entity_description.native_value_fn(self.actuator) + return self.device[self.entity_description.key]["setpoint"] async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index b78fd689cb9..6646cce3369 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -3,9 +3,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Any -from plugwise import DeviceData, Smile +from plugwise import Smile +from plugwise.constants import SelectOptionsType, SelectType from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -22,16 +22,17 @@ from .entity import PlugwiseEntity class PlugwiseSelectDescriptionMixin: """Mixin values for Plugwise Select entities.""" - command: Callable[[Smile, str, str], Awaitable[Any]] - value_fn: Callable[[DeviceData], str] - options_fn: Callable[[DeviceData], list[str]] + command: Callable[[Smile, str, str], Awaitable[None]] + options_key: SelectOptionsType @dataclass class PlugwiseSelectEntityDescription( SelectEntityDescription, PlugwiseSelectDescriptionMixin ): - """Class describing Plugwise Number entities.""" + """Class describing Plugwise Select entities.""" + + key: SelectType SELECT_TYPES = ( @@ -40,8 +41,7 @@ SELECT_TYPES = ( translation_key="select_schedule", icon="mdi:calendar-clock", command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), - value_fn=lambda data: data["selected_schedule"], - options_fn=lambda data: data.get("available_schedules"), + options_key="available_schedules", ), PlugwiseSelectEntityDescription( key="select_regulation_mode", @@ -49,8 +49,7 @@ SELECT_TYPES = ( icon="mdi:hvac", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_regulation_mode(opt), - value_fn=lambda data: data["regulation_mode"], - options_fn=lambda data: data.get("regulation_modes"), + options_key="regulation_modes", ), PlugwiseSelectEntityDescription( key="select_dhw_mode", @@ -58,8 +57,7 @@ SELECT_TYPES = ( icon="mdi:shower", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_dhw_mode(opt), - value_fn=lambda data: data["dhw_mode"], - options_fn=lambda data: data.get("dhw_modes"), + options_key="dhw_modes", ), ) @@ -77,7 +75,7 @@ async def async_setup_entry( entities: list[PlugwiseSelectEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in SELECT_TYPES: - if (options := description.options_fn(device)) and len(options) > 1: + if description.options_key in device: entities.append( PlugwiseSelectEntity(coordinator, device_id, description) ) @@ -100,16 +98,12 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_options = self.device[entity_description.options_key] @property def current_option(self) -> str: """Return the selected entity option to represent the entity state.""" - return self.entity_description.value_fn(self.device) - - @property - def options(self) -> list[str]: - """Return the selectable entity options.""" - return self.entity_description.options_fn(self.device) + return self.device[self.entity_description.key] async def async_select_option(self, option: str) -> None: """Change to the selected entity option.""" diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index d18226e5af9..0cc878178fe 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -1,6 +1,10 @@ """Plugwise Sensor component for Home Assistant.""" from __future__ import annotations +from dataclasses import dataclass + +from plugwise.constants import SensorType + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -27,8 +31,16 @@ from .const import DOMAIN from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity -SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass +class PlugwiseSensorEntityDescription(SensorEntityDescription): + """Describes Plugwise sensor entity.""" + + key: SensorType + + +SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = ( + PlugwiseSensorEntityDescription( key="setpoint", translation_key="setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -36,7 +48,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="setpoint_high", translation_key="cooling_setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -44,7 +56,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="setpoint_low", translation_key="heating_setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -52,14 +64,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="intended_boiler_temperature", translation_key="intended_boiler_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -67,7 +79,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="temperature_difference", translation_key="temperature_difference", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -75,14 +87,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="outdoor_temperature", translation_key="outdoor_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="outdoor_air_temperature", translation_key="outdoor_air_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -90,7 +102,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="water_temperature", translation_key="water_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -98,7 +110,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="return_temperature", translation_key="return_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -106,14 +118,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed", translation_key="electricity_consumed", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced", translation_key="electricity_produced", native_unit_of_measurement=UnitOfPower.WATT, @@ -121,28 +133,28 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_interval", translation_key="electricity_consumed_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_peak_interval", translation_key="electricity_consumed_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_off_peak_interval", translation_key="electricity_consumed_off_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_interval", translation_key="electricity_produced_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -150,133 +162,133 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL, entity_registry_enabled_default=False, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_peak_interval", translation_key="electricity_produced_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_off_peak_interval", translation_key="electricity_produced_off_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_point", translation_key="electricity_consumed_point", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_off_peak_point", translation_key="electricity_consumed_off_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_peak_point", translation_key="electricity_consumed_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_off_peak_cumulative", translation_key="electricity_consumed_off_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_consumed_peak_cumulative", translation_key="electricity_consumed_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_point", translation_key="electricity_produced_point", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_off_peak_point", translation_key="electricity_produced_off_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_peak_point", translation_key="electricity_produced_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_off_peak_cumulative", translation_key="electricity_produced_off_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_produced_peak_cumulative", translation_key="electricity_produced_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_phase_one_consumed", translation_key="electricity_phase_one_consumed", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_phase_two_consumed", translation_key="electricity_phase_two_consumed", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_phase_three_consumed", translation_key="electricity_phase_three_consumed", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_phase_one_produced", translation_key="electricity_phase_one_produced", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_phase_two_produced", translation_key="electricity_phase_two_produced", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="electricity_phase_three_produced", translation_key="electricity_phase_three_produced", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="voltage_phase_one", translation_key="voltage_phase_one", device_class=SensorDeviceClass.VOLTAGE, @@ -284,7 +296,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="voltage_phase_two", translation_key="voltage_phase_two", device_class=SensorDeviceClass.VOLTAGE, @@ -292,7 +304,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="voltage_phase_three", translation_key="voltage_phase_three", device_class=SensorDeviceClass.VOLTAGE, @@ -300,49 +312,49 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="gas_consumed_interval", translation_key="gas_consumed_interval", icon="mdi:meter-gas", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="gas_consumed_cumulative", translation_key="gas_consumed_cumulative", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="net_electricity_point", translation_key="net_electricity_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="net_electricity_cumulative", translation_key="net_electricity_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="illuminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="modulation_level", translation_key="modulation_level", icon="mdi:percent", @@ -350,7 +362,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="valve_position", translation_key="valve_position", icon="mdi:valve", @@ -358,7 +370,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="water_pressure", translation_key="water_pressure", native_unit_of_measurement=UnitOfPressure.BAR, @@ -366,13 +378,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="dhw_temperature", translation_key="dhw_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -380,7 +392,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + PlugwiseSensorEntityDescription( key="domestic_hot_water_setpoint", translation_key="domestic_hot_water_setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -388,14 +400,6 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( - key="maximum_boiler_temperature", - translation_key="maximum_boiler_temperature", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=EntityCategory.DIAGNOSTIC, - state_class=SensorStateClass.MEASUREMENT, - ), ) @@ -409,11 +413,10 @@ async def async_setup_entry( entities: list[PlugwiseSensorEntity] = [] for device_id, device in coordinator.data.devices.items(): + if not (sensors := device.get("sensors")): + continue for description in SENSORS: - if ( - "sensors" not in device - or device["sensors"].get(description.key) is None - ): + if description.key not in sensors: continue entities.append( @@ -430,11 +433,13 @@ async def async_setup_entry( class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): """Represent Plugwise Sensors.""" + entity_description: PlugwiseSensorEntityDescription + def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, device_id: str, - description: SensorEntityDescription, + description: PlugwiseSensorEntityDescription, ) -> None: """Initialise the sensor.""" super().__init__(coordinator, device_id) @@ -442,6 +447,6 @@ class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): self._attr_unique_id = f"{device_id}-{description.key}" @property - def native_value(self) -> int | float | None: + def native_value(self) -> int | float: """Return the value reported by the sensor.""" - return self.device["sensors"].get(self.entity_description.key) + return self.device["sensors"][self.entity_description.key] diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 4204ab5a4d9..8639826e37a 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -1,11 +1,10 @@ """Plugwise Switch component for HomeAssistant.""" from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass from typing import Any -from plugwise import SmileSwitches +from plugwise.constants import SwitchType from homeassistant.components.switch import ( SwitchDeviceClass, @@ -24,16 +23,11 @@ from .util import plugwise_command @dataclass -class PlugwiseSwitchBaseMixin: - """Mixin for required Plugwise switch description keys.""" - - value_fn: Callable[[SmileSwitches], bool] - - -@dataclass -class PlugwiseSwitchEntityDescription(SwitchEntityDescription, PlugwiseSwitchBaseMixin): +class PlugwiseSwitchEntityDescription(SwitchEntityDescription): """Describes Plugwise switch entity.""" + key: SwitchType + SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = ( PlugwiseSwitchEntityDescription( @@ -41,27 +35,24 @@ SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = ( translation_key="dhw_cm_switch", icon="mdi:water-plus", entity_category=EntityCategory.CONFIG, - value_fn=lambda data: data["dhw_cm_switch"], ), PlugwiseSwitchEntityDescription( key="lock", translation_key="lock", icon="mdi:lock", entity_category=EntityCategory.CONFIG, - value_fn=lambda data: data["lock"], ), PlugwiseSwitchEntityDescription( key="relay", translation_key="relay", device_class=SwitchDeviceClass.SWITCH, - value_fn=lambda data: data["relay"], ), PlugwiseSwitchEntityDescription( key="cooling_ena_switch", + translation_key="cooling_ena_switch", name="Cooling", icon="mdi:snowflake-thermometer", entity_category=EntityCategory.CONFIG, - value_fn=lambda data: data["cooling_ena_switch"], ), ) @@ -103,7 +94,7 @@ class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): @property def is_on(self) -> bool: """Return True if entity is on.""" - return self.entity_description.value_fn(self.device["switches"]) + return self.device["switches"][self.entity_description.key] @plugwise_command async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 1aac3651bc8..7cc1ca2cdc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1429,7 +1429,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.31.1 +plugwise==0.31.9 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9736a8d176a..7e7bc5dc41f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1080,7 +1080,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.31.1 +plugwise==0.31.9 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index ae396388639..a97d312cd54 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -6,9 +6,10 @@ import json from typing import Any from unittest.mock import AsyncMock, MagicMock, patch +from plugwise import PlugwiseData import pytest -from homeassistant.components.plugwise.const import API, DOMAIN, PW_TYPE +from homeassistant.components.plugwise.const import DOMAIN from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -39,7 +40,6 @@ def mock_config_entry() -> MockConfigEntry: CONF_PASSWORD: "test-password", CONF_PORT: 80, CONF_USERNAME: "smile", - PW_TYPE: API, }, unique_id="smile98765", ) @@ -90,7 +90,10 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -116,7 +119,10 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -142,7 +148,39 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) + + yield smile + + +@pytest.fixture +def mock_smile_adam_4() -> Generator[None, MagicMock, None]: + """Create a 4th Mock Adam environment for testing exceptions.""" + chosen_env = "adam_jip" + + with patch( + "homeassistant.components.plugwise.coordinator.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "b5c2386c6f6342669e50fe49dd05b188" + smile.heater_id = "e4684553153b44afbef2200885f379dc" + smile.smile_version = "3.2.8" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" + smile.smile_name = "Adam" + + smile.connect.return_value = True + + smile.notifications = _read_json(chosen_env, "notifications") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -167,7 +205,10 @@ def mock_smile_anna() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -192,7 +233,10 @@ def mock_smile_anna_2() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -217,7 +261,10 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -242,7 +289,10 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -250,7 +300,7 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: @pytest.fixture def mock_smile_p1_2() -> Generator[None, MagicMock, None]: """Create a Mock P1 3-phase DSMR environment for testing exceptions.""" - chosen_env = "p1v4_3ph" + chosen_env = "p1v4_442_triple" with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: @@ -267,7 +317,10 @@ def mock_smile_p1_2() -> Generator[None, MagicMock, None]: smile.connect.return_value = True smile.notifications = _read_json(chosen_env, "notifications") - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile @@ -290,7 +343,10 @@ def mock_stretch() -> Generator[None, MagicMock, None]: smile.smile_name = "Stretch" smile.connect.return_value = True - smile.async_update.return_value = _read_json(chosen_env, "all_data") + all_data = _read_json(chosen_env, "all_data") + smile.async_update.return_value = PlugwiseData( + all_data["gateway"], all_data["devices"] + ) yield smile diff --git a/tests/components/plugwise/fixtures/adam_jip/all_data.json b/tests/components/plugwise/fixtures/adam_jip/all_data.json new file mode 100644 index 00000000000..177478f0fff --- /dev/null +++ b/tests/components/plugwise/fixtures/adam_jip/all_data.json @@ -0,0 +1,266 @@ +{ + "devices": { + "1346fbd8498d4dbcab7e18d51b771f3d": { + "active_preset": "no_frost", + "available": true, + "available_schedules": ["None"], + "control_state": "off", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": null, + "location": "06aecb3d00354375924f50c47af36bd2", + "mode": "heat", + "model": "Lisa", + "name": "Slaapkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", + "sensors": { + "battery": 92, + "setpoint": 13.0, + "temperature": 24.2 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03" + }, + "1da4d325838e4ad8aac12177214505c9": { + "available": true, + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "d58fec52899f4f1c92e4f8fad6d8c48c", + "model": "Tom/Floor", + "name": "Tom Logeerkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 28.8, + "temperature_difference": 2.0, + "valve_position": 0.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "356b65335e274d769c338223e7af9c33": { + "available": true, + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "06aecb3d00354375924f50c47af36bd2", + "model": "Tom/Floor", + "name": "Tom Slaapkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 24.3, + "temperature_difference": 1.7, + "valve_position": 0.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05" + }, + "457ce8414de24596a2d5e7dbc9c7682f": { + "available": true, + "dev_class": "zz_misc", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "model": "lumi.plug.maeu01", + "name": "Plug", + "sensors": { + "electricity_consumed_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": false + }, + "vendor": "LUMI", + "zigbee_mac_address": "ABCD012345670A06" + }, + "6f3e9d7084214c21b9dfa46f6eeb8700": { + "active_preset": "home", + "available": true, + "available_schedules": ["None"], + "control_state": "off", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": null, + "location": "d27aede973b54be484f6842d1b2802ad", + "mode": "heat", + "model": "Lisa", + "name": "Kinderkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", + "sensors": { + "battery": 79, + "setpoint": 13.0, + "temperature": 30.0 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "833de10f269c4deab58fb9df69901b4e": { + "available": true, + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "13228dab8ce04617af318a2888b3c548", + "model": "Tom/Floor", + "name": "Tom Woonkamer", + "sensors": { + "setpoint": 9.0, + "temperature": 24.0, + "temperature_difference": 1.8, + "valve_position": 100 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09" + }, + "a6abc6a129ee499c88a4d420cc413b47": { + "active_preset": "home", + "available": true, + "available_schedules": ["None"], + "control_state": "off", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": null, + "location": "d58fec52899f4f1c92e4f8fad6d8c48c", + "mode": "heat", + "model": "Lisa", + "name": "Logeerkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", + "sensors": { + "battery": 80, + "setpoint": 13.0, + "temperature": 30.0 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "b5c2386c6f6342669e50fe49dd05b188": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.2.8", + "hardware": "AME Smile 2.0 board", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Adam", + "regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"], + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": 24.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + }, + "d4496250d0e942cfa7aea3476e9070d5": { + "available": true, + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "d27aede973b54be484f6842d1b2802ad", + "model": "Tom/Floor", + "name": "Tom Kinderkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 28.7, + "temperature_difference": 1.9, + "valve_position": 0.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" + }, + "e4684553153b44afbef2200885f379dc": { + "available": true, + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 20.0, + "resolution": 0.01, + "setpoint": 90.0, + "upper_bound": 90.0 + }, + "model": "10.20", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 0.0, + "modulation_level": 0.0, + "return_temperature": 37.1, + "water_pressure": 1.4, + "water_temperature": 37.3 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Remeha B.V." + }, + "f61f1a2535f54f52ad006a3d18e459ca": { + "active_preset": "home", + "available": true, + "available_schedules": ["None"], + "control_state": "off", + "dev_class": "zone_thermometer", + "firmware": "2020-09-01T02:00:00+02:00", + "hardware": "1", + "last_used": null, + "location": "13228dab8ce04617af318a2888b3c548", + "mode": "heat", + "model": "Jip", + "name": "Woonkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", + "sensors": { + "battery": 100, + "humidity": 56.2, + "setpoint": 9.0, + "temperature": 27.4 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.01, + "setpoint": 9.0, + "upper_bound": 30.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08" + } + }, + "gateway": { + "cooling_present": false, + "gateway_id": "b5c2386c6f6342669e50fe49dd05b188", + "heater_id": "e4684553153b44afbef2200885f379dc", + "notifications": {}, + "smile_name": "Adam" + } +} diff --git a/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json b/tests/components/plugwise/fixtures/adam_jip/notifications.json similarity index 100% rename from tests/components/plugwise/fixtures/p1v4_3ph/notifications.json rename to tests/components/plugwise/fixtures/adam_jip/notifications.json diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index d62ff0e249d..63f0012ea92 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -1,164 +1,32 @@ -[ - { - "smile_name": "Adam", - "gateway_id": "fe799307f1624099878210aa0b9f1475", - "heater_id": "90986d591dcd426cae3ec3e8111ff730", - "cooling_present": false, - "notifications": { - "af82e4ccf9c548528166d38e560662a4": { - "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." - } - } - }, - { - "df4a4a8169904cdb9c03d61a21f42140": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-27T02:00:00+02:00", - "hardware": "255", - "location": "12493538af164a409c6a1c79e38afe1c", - "model": "Lisa", - "name": "Zone Lisa Bios", - "zigbee_mac_address": "ABCD012345670A06", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 13.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01 +{ + "devices": { + "02cf28bfec924855854c544690a609ef": { + "available": true, + "dev_class": "vcr", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "name": "NVR", + "sensors": { + "electricity_consumed": 34.0, + "electricity_consumed_interval": 9.15, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 }, - "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "away", - "available_schedules": [ - "CV Roan", - "Bios Schema met Film Avond", - "GF7 Woonkamer", - "Badkamer Schema", - "CV Jessie" - ], - "selected_schedule": "None", - "last_used": "Badkamer Schema", - "mode": "heat", - "sensors": { - "temperature": 16.5, - "setpoint": 13.0, - "battery": 67 - } - }, - "b310b72a0e354bfab43089919b9a88bf": { - "dev_class": "thermo_sensor", - "firmware": "2019-03-27T01:00:00+01:00", - "hardware": "1", - "location": "c50f167537524366a5af7aa3942feb1e", - "model": "Tom/Floor", - "name": "Floor kraan", - "zigbee_mac_address": "ABCD012345670A02", - "vendor": "Plugwise", - "available": true, - "sensors": { - "temperature": 26.0, - "setpoint": 21.5, - "temperature_difference": 3.5, - "valve_position": 100 - } - }, - "a2c3583e0a6349358998b760cea82d2a": { - "dev_class": "thermo_sensor", - "firmware": "2019-03-27T01:00:00+01:00", - "hardware": "1", - "location": "12493538af164a409c6a1c79e38afe1c", - "model": "Tom/Floor", - "name": "Bios Cv Thermostatic Radiator ", - "zigbee_mac_address": "ABCD012345670A09", - "vendor": "Plugwise", - "available": true, - "sensors": { - "temperature": 17.2, - "setpoint": 13.0, - "battery": 62, - "temperature_difference": -0.2, - "valve_position": 0.0 - } - }, - "b59bcebaf94b499ea7d46e4a66fb62d8": { - "dev_class": "zone_thermostat", - "firmware": "2016-08-02T02:00:00+02:00", - "hardware": "255", - "location": "c50f167537524366a5af7aa3942feb1e", - "model": "Lisa", - "name": "Zone Lisa WK", - "zigbee_mac_address": "ABCD012345670A07", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 21.5, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01 + "switches": { + "lock": true, + "relay": true }, - "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "home", - "available_schedules": [ - "CV Roan", - "Bios Schema met Film Avond", - "GF7 Woonkamer", - "Badkamer Schema", - "CV Jessie" - ], - "selected_schedule": "GF7 Woonkamer", - "last_used": "GF7 Woonkamer", - "mode": "auto", - "sensors": { - "temperature": 20.9, - "setpoint": 21.5, - "battery": 34 - } - }, - "fe799307f1624099878210aa0b9f1475": { - "dev_class": "gateway", - "firmware": "3.0.15", - "hardware": "AME Smile 2.0 board", - "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", - "mac_address": "012345670001", - "model": "Gateway", - "name": "Adam", - "zigbee_mac_address": "ABCD012345670101", "vendor": "Plugwise", - "regulation_mode": "heating", - "binary_sensors": { - "plugwise_notification": true - }, - "sensors": { - "outdoor_temperature": 7.81 - } - }, - "d3da73bde12a47d5a6b8f9dad971f2ec": { - "dev_class": "thermo_sensor", - "firmware": "2019-03-27T01:00:00+01:00", - "hardware": "1", - "location": "82fa13f017d240daa0d0ea1775420f24", - "model": "Tom/Floor", - "name": "Thermostatic Radiator Jessie", - "zigbee_mac_address": "ABCD012345670A10", - "vendor": "Plugwise", - "available": true, - "sensors": { - "temperature": 17.1, - "setpoint": 15.0, - "battery": 62, - "temperature_difference": 0.1, - "valve_position": 0.0 - } + "zigbee_mac_address": "ABCD012345670A15" }, "21f2b542c49845e6bb416884c55778d6": { + "available": true, "dev_class": "game_console", "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", "model": "Plug", "name": "Playstation Smart Plug", - "zigbee_mac_address": "ABCD012345670A12", - "vendor": "Plugwise", - "available": true, "sensors": { "electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, @@ -166,19 +34,111 @@ "electricity_produced_interval": 0.0 }, "switches": { - "relay": true, - "lock": false - } + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A12" + }, + "4a810418d5394b3f82727340b91ba740": { + "available": true, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "name": "USG Smart Plug", + "sensors": { + "electricity_consumed": 8.5, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A16" + }, + "675416a629f343c495449970e2ca37b5": { + "available": true, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "name": "Ziggo Modem", + "sensors": { + "electricity_consumed": 12.2, + "electricity_consumed_interval": 2.97, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "680423ff840043738f42cc7f1ff97a36": { + "available": true, + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "08963fec7c53423ca5680aa4cb502c63", + "model": "Tom/Floor", + "name": "Thermostatic Radiator Badkamer", + "sensors": { + "battery": 51, + "setpoint": 14.0, + "temperature": 19.1, + "temperature_difference": -0.4, + "valve_position": 0.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A17" + }, + "6a3bf693d05e48e0b460c815a4fdd09d": { + "active_preset": "asleep", + "available": true, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": "CV Jessie", + "location": "82fa13f017d240daa0d0ea1775420f24", + "mode": "auto", + "model": "Lisa", + "name": "Zone Thermostat Jessie", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "CV Jessie", + "sensors": { + "battery": 37, + "setpoint": 15.0, + "temperature": 17.2 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03" }, "78d1126fc4c743db81b61c20e88342a7": { + "available": true, "dev_class": "central_heating_pump", "firmware": "2019-06-21T02:00:00+02:00", "location": "c50f167537524366a5af7aa3942feb1e", "model": "Plug", "name": "CV Pomp", - "zigbee_mac_address": "ABCD012345670A05", - "vendor": "Plugwise", - "available": true, "sensors": { "electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, @@ -187,91 +147,31 @@ }, "switches": { "relay": true - } + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05" }, "90986d591dcd426cae3ec3e8111ff730": { + "binary_sensors": { + "heating_state": true + }, "dev_class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", "model": "Unknown", "name": "OnOff", - "binary_sensors": { - "heating_state": true - }, "sensors": { - "water_temperature": 70.0, "intended_boiler_temperature": 70.0, - "modulation_level": 1 - } - }, - "cd0ddb54ef694e11ac18ed1cbce5dbbd": { - "dev_class": "vcr", - "firmware": "2019-06-21T02:00:00+02:00", - "location": "cd143c07248f491493cea0533bc3d669", - "model": "Plug", - "name": "NAS", - "zigbee_mac_address": "ABCD012345670A14", - "vendor": "Plugwise", - "available": true, - "sensors": { - "electricity_consumed": 16.5, - "electricity_consumed_interval": 0.5, - "electricity_produced": 0.0, - "electricity_produced_interval": 0.0 - }, - "switches": { - "relay": true, - "lock": true - } - }, - "4a810418d5394b3f82727340b91ba740": { - "dev_class": "router", - "firmware": "2019-06-21T02:00:00+02:00", - "location": "cd143c07248f491493cea0533bc3d669", - "model": "Plug", - "name": "USG Smart Plug", - "zigbee_mac_address": "ABCD012345670A16", - "vendor": "Plugwise", - "available": true, - "sensors": { - "electricity_consumed": 8.5, - "electricity_consumed_interval": 0.0, - "electricity_produced": 0.0, - "electricity_produced_interval": 0.0 - }, - "switches": { - "relay": true, - "lock": true - } - }, - "02cf28bfec924855854c544690a609ef": { - "dev_class": "vcr", - "firmware": "2019-06-21T02:00:00+02:00", - "location": "cd143c07248f491493cea0533bc3d669", - "model": "Plug", - "name": "NVR", - "zigbee_mac_address": "ABCD012345670A15", - "vendor": "Plugwise", - "available": true, - "sensors": { - "electricity_consumed": 34.0, - "electricity_consumed_interval": 9.15, - "electricity_produced": 0.0, - "electricity_produced_interval": 0.0 - }, - "switches": { - "relay": true, - "lock": true + "modulation_level": 1, + "water_temperature": 70.0 } }, "a28f588dc4a049a483fd03a30361ad3a": { + "available": true, "dev_class": "settop", "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", "model": "Plug", "name": "Fibaro HC2", - "zigbee_mac_address": "ABCD012345670A13", - "vendor": "Plugwise", - "available": true, "sensors": { "electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, @@ -279,80 +179,50 @@ "electricity_produced_interval": 0.0 }, "switches": { - "relay": true, - "lock": true - } - }, - "6a3bf693d05e48e0b460c815a4fdd09d": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-27T02:00:00+02:00", - "hardware": "255", - "location": "82fa13f017d240daa0d0ea1775420f24", - "model": "Lisa", - "name": "Zone Thermostat Jessie", - "zigbee_mac_address": "ABCD012345670A03", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 15.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01 + "lock": true, + "relay": true }, - "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "asleep", - "available_schedules": [ - "CV Roan", - "Bios Schema met Film Avond", - "GF7 Woonkamer", - "Badkamer Schema", - "CV Jessie" - ], - "selected_schedule": "CV Jessie", - "last_used": "CV Jessie", - "mode": "auto", - "sensors": { - "temperature": 17.2, - "setpoint": 15.0, - "battery": 37 - } + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A13" }, - "680423ff840043738f42cc7f1ff97a36": { + "a2c3583e0a6349358998b760cea82d2a": { + "available": true, "dev_class": "thermo_sensor", "firmware": "2019-03-27T01:00:00+01:00", "hardware": "1", - "location": "08963fec7c53423ca5680aa4cb502c63", + "location": "12493538af164a409c6a1c79e38afe1c", "model": "Tom/Floor", - "name": "Thermostatic Radiator Badkamer", - "zigbee_mac_address": "ABCD012345670A17", - "vendor": "Plugwise", - "available": true, + "name": "Bios Cv Thermostatic Radiator ", "sensors": { - "temperature": 19.1, - "setpoint": 14.0, - "battery": 51, - "temperature_difference": -0.4, + "battery": 62, + "setpoint": 13.0, + "temperature": 17.2, + "temperature_difference": -0.2, "valve_position": 0.0 - } - }, - "f1fee6043d3642a9b0a65297455f008e": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-27T02:00:00+02:00", - "hardware": "255", - "location": "08963fec7c53423ca5680aa4cb502c63", - "model": "Lisa", - "name": "Zone Thermostat Badkamer", - "zigbee_mac_address": "ABCD012345670A08", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 14.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01 }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09" + }, + "b310b72a0e354bfab43089919b9a88bf": { + "available": true, + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Tom/Floor", + "name": "Floor kraan", + "sensors": { + "setpoint": 21.5, + "temperature": 26.0, + "temperature_difference": 3.5, + "valve_position": 100 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "b59bcebaf94b499ea7d46e4a66fb62d8": { + "active_preset": "home", "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "away", "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -360,53 +230,71 @@ "Badkamer Schema", "CV Jessie" ], - "selected_schedule": "Badkamer Schema", - "last_used": "Badkamer Schema", + "dev_class": "zone_thermostat", + "firmware": "2016-08-02T02:00:00+02:00", + "hardware": "255", + "last_used": "GF7 Woonkamer", + "location": "c50f167537524366a5af7aa3942feb1e", "mode": "auto", + "model": "Lisa", + "name": "Zone Lisa WK", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "GF7 Woonkamer", "sensors": { - "temperature": 18.9, - "setpoint": 14.0, - "battery": 92 - } + "battery": 34, + "setpoint": 21.5, + "temperature": 20.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 21.5, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" }, - "675416a629f343c495449970e2ca37b5": { - "dev_class": "router", + "cd0ddb54ef694e11ac18ed1cbce5dbbd": { + "available": true, + "dev_class": "vcr", "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", "model": "Plug", - "name": "Ziggo Modem", - "zigbee_mac_address": "ABCD012345670A01", - "vendor": "Plugwise", - "available": true, + "name": "NAS", "sensors": { - "electricity_consumed": 12.2, - "electricity_consumed_interval": 2.97, + "electricity_consumed": 16.5, + "electricity_consumed_interval": 0.5, "electricity_produced": 0.0, "electricity_produced_interval": 0.0 }, "switches": { - "relay": true, - "lock": true - } + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A14" }, - "e7693eb9582644e5b865dba8d4447cf1": { - "dev_class": "thermostatic_radiator_valve", + "d3da73bde12a47d5a6b8f9dad971f2ec": { + "available": true, + "dev_class": "thermo_sensor", "firmware": "2019-03-27T01:00:00+01:00", "hardware": "1", - "location": "446ac08dd04d4eff8ac57489757b7314", + "location": "82fa13f017d240daa0d0ea1775420f24", "model": "Tom/Floor", - "name": "CV Kraan Garage", - "zigbee_mac_address": "ABCD012345670A11", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 5.5, - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01 + "name": "Thermostatic Radiator Jessie", + "sensors": { + "battery": 62, + "setpoint": 15.0, + "temperature": 17.1, + "temperature_difference": 0.1, + "valve_position": 0.0 }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A10" + }, + "df4a4a8169904cdb9c03d61a21f42140": { + "active_preset": "away", "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "no_frost", "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -414,16 +302,128 @@ "Badkamer Schema", "CV Jessie" ], - "selected_schedule": "None", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "last_used": "Badkamer Schema", + "location": "12493538af164a409c6a1c79e38afe1c", "mode": "heat", + "model": "Lisa", + "name": "Zone Lisa Bios", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", + "sensors": { + "battery": 67, + "setpoint": 13.0, + "temperature": 16.5 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A06" + }, + "e7693eb9582644e5b865dba8d4447cf1": { + "active_preset": "no_frost", + "available": true, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "last_used": "Badkamer Schema", + "location": "446ac08dd04d4eff8ac57489757b7314", + "mode": "heat", + "model": "Tom/Floor", + "name": "CV Kraan Garage", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", "sensors": { - "temperature": 15.6, - "setpoint": 5.5, "battery": 68, + "setpoint": 5.5, + "temperature": 15.6, "temperature_difference": 0.0, "valve_position": 0.0 - } + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 5.5, + "upper_bound": 100.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A11" + }, + "f1fee6043d3642a9b0a65297455f008e": { + "active_preset": "away", + "available": true, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": "Badkamer Schema", + "location": "08963fec7c53423ca5680aa4cb502c63", + "mode": "auto", + "model": "Lisa", + "name": "Zone Thermostat Badkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Badkamer Schema", + "sensors": { + "battery": 92, + "setpoint": 14.0, + "temperature": 18.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 14.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08" + }, + "fe799307f1624099878210aa0b9f1475": { + "binary_sensors": { + "plugwise_notification": true + }, + "dev_class": "gateway", + "firmware": "3.0.15", + "hardware": "AME Smile 2.0 board", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Adam", + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": 7.81 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" } + }, + "gateway": { + "cooling_present": false, + "gateway_id": "fe799307f1624099878210aa0b9f1475", + "heater_id": "90986d591dcd426cae3ec3e8111ff730", + "notifications": { + "af82e4ccf9c548528166d38e560662a4": { + "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." + } + }, + "smile_name": "Adam" } -] +} diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index f00293a6554..49b5221233f 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -1,48 +1,9 @@ -[ - { - "smile_name": "Smile Anna", - "gateway_id": "015ae9ea3f964e668e490fa39da3870b", - "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", - "cooling_present": false, - "notifications": {} - }, - { - "1cbf783bb11e4a7c8a6843dee3a86927": { - "dev_class": "heater_central", - "location": "a57efe5f145f498c9be62a9b63626fbf", - "model": "Generic heater/cooler", - "name": "OpenTherm", - "vendor": "Techneco", - "maximum_boiler_temperature": { - "setpoint": 60.0, - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 1.0 - }, - "available": true, - "binary_sensors": { - "cooling_enabled": false, - "dhw_state": false, - "heating_state": true, - "compressor_state": true, - "slave_boiler_state": false, - "flame_state": false - }, - "sensors": { - "water_temperature": 29.1, - "domestic_hot_water_setpoint": 60.0, - "dhw_temperature": 46.3, - "intended_boiler_temperature": 35.0, - "modulation_level": 52, - "return_temperature": 25.1, - "water_pressure": 1.57, - "outdoor_air_temperature": 3.0 - }, - "switches": { - "dhw_cm_switch": false - } - }, +{ + "devices": { "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "4.0.15", "hardware": "AME Smile 2.0 board", @@ -50,41 +11,85 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Smile Anna", - "vendor": "Plugwise", - "binary_sensors": { - "plugwise_notification": false - }, "sensors": { "outdoor_temperature": 20.2 - } + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": true, + "cooling_enabled": false, + "dhw_state": false, + "flame_state": false, + "heating_state": true, + "slave_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 46.3, + "intended_boiler_temperature": 35.0, + "modulation_level": 52, + "outdoor_air_temperature": 3.0, + "return_temperature": 25.1, + "water_pressure": 1.57, + "water_temperature": 29.1 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" }, "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", + "last_used": "standaard", "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "mode": "auto", "model": "ThermoTouch", "name": "Anna", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 20.5, - "lower_bound": 4.0, - "upper_bound": 30.0, - "resolution": 0.1 - }, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], - "active_preset": "home", - "available_schedules": ["standaard"], - "selected_schedule": "standaard", - "last_used": "standaard", - "mode": "auto", + "select_schedule": "standaard", "sensors": { - "temperature": 19.3, - "setpoint": 20.5, - "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4.0 - } + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint": 20.5, + "temperature": 19.3 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" } + }, + "gateway": { + "cooling_present": false, + "gateway_id": "015ae9ea3f964e668e490fa39da3870b", + "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", + "notifications": {}, + "smile_name": "Smile Anna" } -] +} diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 06a3fa400bf..92618a90189 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -1,88 +1,80 @@ -[ - { - "smile_name": "Adam", - "gateway_id": "da224107914542988a88561b4452b0f6", - "heater_id": "056ee145a816487eaa69243c3280f8bf", - "cooling_present": true, - "notifications": {} - }, - { - "ad4838d7d35c4d6ea796ee12ae5aedf8": { - "dev_class": "thermostat", - "location": "f2bf9048bef64cc5b6d5110154e33c81", - "model": "ThermoTouch", - "name": "Anna", - "vendor": "Plugwise", - "thermostat": { - "setpoint_low": 4.0, - "setpoint_high": 23.5, - "lower_bound": 1.0, - "upper_bound": 35.0, - "resolution": 0.01 - }, +{ + "devices": { + "056ee145a816487eaa69243c3280f8bf": { "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "asleep", - "available_schedules": ["Weekschema", "Badkamer", "Test"], - "selected_schedule": "None", - "last_used": "Weekschema", - "control_state": "cooling", - "mode": "heat_cool", + "binary_sensors": { + "cooling_state": true, + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", "sensors": { - "temperature": 25.8, - "setpoint_low": 4.0, - "setpoint_high": 23.5 + "intended_boiler_temperature": 17.5, + "water_temperature": 19.0 + }, + "switches": { + "dhw_cm_switch": false } }, "1772a4ea304041adb83f357b751341ff": { + "available": true, "dev_class": "thermo_sensor", "firmware": "2020-11-04T01:00:00+01:00", "hardware": "1", "location": "f871b8c4d63549319221e294e4f88074", "model": "Tom/Floor", "name": "Tom Badkamer", - "zigbee_mac_address": "ABCD012345670A01", - "vendor": "Plugwise", - "available": true, "sensors": { - "temperature": 21.6, "battery": 99, + "temperature": 21.6, "temperature_difference": 2.3, "valve_position": 0.0 - } - }, - "e2f4322d57924fa090fbbc48b3a140dc": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-10T02:00:00+02:00", - "hardware": "255", - "location": "f871b8c4d63549319221e294e4f88074", - "model": "Lisa", - "name": "Lisa Badkamer", - "zigbee_mac_address": "ABCD012345670A04", - "vendor": "Plugwise", - "thermostat": { - "setpoint_low": 19.0, - "setpoint_high": 25.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01 }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "active_preset": "asleep", "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "home", "available_schedules": ["Weekschema", "Badkamer", "Test"], - "selected_schedule": "Badkamer", - "last_used": "Badkamer", - "control_state": "off", - "mode": "auto", + "control_state": "cooling", + "dev_class": "thermostat", + "last_used": "Weekschema", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "mode": "heat_cool", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Weekschema", + "selected_schedule": "None", "sensors": { - "temperature": 239, - "battery": 56, - "setpoint_low": 20.0, - "setpoint_high": 23.5 - } + "setpoint_high": 23.5, + "setpoint_low": 4.0, + "temperature": 25.8 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint_high": 23.5, + "setpoint_low": 4.0, + "upper_bound": 35.0 + }, + "vendor": "Plugwise" }, "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "3.6.4", "hardware": "AME Smile 2.0 board", @@ -90,60 +82,70 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Adam", - "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise", "regulation_mode": "cooling", "regulation_modes": [ - "cooling", "heating", "off", "bleeding_cold", - "bleeding_hot" + "bleeding_hot", + "cooling" ], - "binary_sensors": { - "plugwise_notification": false - }, + "select_regulation_mode": "heating", "sensors": { "outdoor_temperature": 29.65 - } + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" }, - "056ee145a816487eaa69243c3280f8bf": { - "dev_class": "heater_central", - "location": "bc93488efab249e5bc54fd7e175a6f91", - "model": "Generic heater", - "name": "OpenTherm", - "maximum_boiler_temperature": { - "setpoint": 60.0, - "lower_bound": 25.0, - "upper_bound": 95.0, - "resolution": 0.01 - }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "active_preset": "home", "available": true, - "binary_sensors": { - "cooling_state": true, - "dhw_state": false, - "heating_state": false, - "flame_state": false - }, + "available_schedules": ["Weekschema", "Badkamer", "Test"], + "control_state": "off", + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "last_used": "Badkamer", + "location": "f871b8c4d63549319221e294e4f88074", + "mode": "auto", + "model": "Lisa", + "name": "Lisa Badkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Badkamer", "sensors": { - "water_temperature": 19.0, - "intended_boiler_temperature": 17.5 + "battery": 56, + "setpoint_high": 23.5, + "setpoint_low": 20.0, + "temperature": 239 }, - "switches": { - "dhw_cm_switch": false - } + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint_high": 25.0, + "setpoint_low": 19.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" }, "e8ef2a01ed3b4139a53bf749204fe6b4": { "dev_class": "switching", - "model": "Switchgroup", - "name": "Test", "members": [ "2568cc4b9c1e401495d4741a5f89bee1", "29542b2b6a6a4169acecc15c72a599b8" ], + "model": "Switchgroup", + "name": "Test", "switches": { "relay": true } } + }, + "gateway": { + "cooling_present": true, + "gateway_id": "da224107914542988a88561b4452b0f6", + "heater_id": "056ee145a816487eaa69243c3280f8bf", + "notifications": {}, + "smile_name": "Adam" } -] +} diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 8ee3df544e5..4345cf76a3a 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -1,81 +1,83 @@ -[ - { - "smile_name": "Adam", - "gateway_id": "da224107914542988a88561b4452b0f6", - "heater_id": "056ee145a816487eaa69243c3280f8bf", - "cooling_present": false, - "notifications": {} - }, - { - "ad4838d7d35c4d6ea796ee12ae5aedf8": { - "dev_class": "thermostat", - "location": "f2bf9048bef64cc5b6d5110154e33c81", - "model": "ThermoTouch", - "name": "Anna", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 20.0, - "lower_bound": 1.0, - "upper_bound": 35.0, - "resolution": 0.01 - }, +{ + "devices": { + "056ee145a816487eaa69243c3280f8bf": { "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "asleep", - "available_schedules": ["Weekschema", "Badkamer", "Test"], - "selected_schedule": "None", - "last_used": "Weekschema", - "control_state": "heating", - "mode": "heat", - "sensors": { "temperature": 19.1, "setpoint": 20.0 } + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": true + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 38.1, + "water_temperature": 37.0 + }, + "switches": { + "dhw_cm_switch": false + } }, "1772a4ea304041adb83f357b751341ff": { + "available": true, "dev_class": "thermo_sensor", "firmware": "2020-11-04T01:00:00+01:00", "hardware": "1", "location": "f871b8c4d63549319221e294e4f88074", "model": "Tom/Floor", "name": "Tom Badkamer", - "zigbee_mac_address": "ABCD012345670A01", - "vendor": "Plugwise", - "available": true, "sensors": { - "temperature": 18.6, "battery": 99, + "temperature": 18.6, "temperature_difference": 2.3, "valve_position": 0.0 - } - }, - "e2f4322d57924fa090fbbc48b3a140dc": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-10T02:00:00+02:00", - "hardware": "255", - "location": "f871b8c4d63549319221e294e4f88074", - "model": "Lisa", - "name": "Lisa Badkamer", - "zigbee_mac_address": "ABCD012345670A04", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 15.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01 }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "active_preset": "asleep", "available": true, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "home", "available_schedules": ["Weekschema", "Badkamer", "Test"], - "selected_schedule": "Badkamer", - "last_used": "Badkamer", - "control_state": "off", - "mode": "auto", + "control_state": "heating", + "dev_class": "thermostat", + "last_used": "Weekschema", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "mode": "heat", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Weekschema", + "selected_schedule": "None", "sensors": { - "temperature": 17.9, - "battery": 56, - "setpoint": 15.0 - } + "setpoint": 20.0, + "temperature": 19.1 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint": 20.0, + "upper_bound": 35.0 + }, + "vendor": "Plugwise" }, "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "3.6.4", "hardware": "AME Smile 2.0 board", @@ -83,59 +85,62 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Adam", - "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise", "regulation_mode": "heating", "regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"], - "binary_sensors": { - "plugwise_notification": false - }, + "select_regulation_mode": "heating", "sensors": { "outdoor_temperature": -1.25 - } + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" }, - "056ee145a816487eaa69243c3280f8bf": { - "dev_class": "heater_central", - "location": "bc93488efab249e5bc54fd7e175a6f91", - "model": "Generic heater", - "name": "OpenTherm", - "maximum_boiler_temperature": { - "setpoint": 60.0, - "lower_bound": 25.0, - "upper_bound": 95.0, - "resolution": 0.01 - }, - "domestic_hot_water_setpoint": { - "setpoint": 60.0, - "lower_bound": 40.0, - "upper_bound": 60.0, - "resolution": 0.01 - }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "active_preset": "home", "available": true, - "binary_sensors": { - "dhw_state": false, - "heating_state": true, - "flame_state": false - }, + "available_schedules": ["Weekschema", "Badkamer", "Test"], + "control_state": "off", + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "last_used": "Badkamer", + "location": "f871b8c4d63549319221e294e4f88074", + "mode": "auto", + "model": "Lisa", + "name": "Lisa Badkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Badkamer", "sensors": { - "water_temperature": 37.0, - "intended_boiler_temperature": 38.1 + "battery": 56, + "setpoint": 15.0, + "temperature": 17.9 }, - "switches": { - "dhw_cm_switch": false - } + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 99.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" }, "e8ef2a01ed3b4139a53bf749204fe6b4": { "dev_class": "switching", - "model": "Switchgroup", - "name": "Test", "members": [ "2568cc4b9c1e401495d4741a5f89bee1", "29542b2b6a6a4169acecc15c72a599b8" ], + "model": "Switchgroup", + "name": "Test", "switches": { "relay": true } } + }, + "gateway": { + "cooling_present": false, + "gateway_id": "da224107914542988a88561b4452b0f6", + "heater_id": "056ee145a816487eaa69243c3280f8bf", + "notifications": {}, + "smile_name": "Adam" } -] +} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index ba980a7fce3..20f2db213bd 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -1,49 +1,9 @@ -[ - { - "smile_name": "Smile Anna", - "gateway_id": "015ae9ea3f964e668e490fa39da3870b", - "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", - "cooling_present": true, - "notifications": {} - }, - { - "1cbf783bb11e4a7c8a6843dee3a86927": { - "dev_class": "heater_central", - "location": "a57efe5f145f498c9be62a9b63626fbf", - "model": "Generic heater/cooler", - "name": "OpenTherm", - "vendor": "Techneco", - "maximum_boiler_temperature": { - "setpoint": 60.0, - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 1.0 - }, - "available": true, - "binary_sensors": { - "cooling_enabled": true, - "dhw_state": false, - "heating_state": false, - "compressor_state": true, - "cooling_state": true, - "slave_boiler_state": false, - "flame_state": false - }, - "sensors": { - "water_temperature": 22.7, - "domestic_hot_water_setpoint": 60.0, - "dhw_temperature": 41.5, - "intended_boiler_temperature": 0.0, - "modulation_level": 40, - "return_temperature": 23.8, - "water_pressure": 1.57, - "outdoor_air_temperature": 28.0 - }, - "switches": { - "dhw_cm_switch": false - } - }, +{ + "devices": { "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "4.0.15", "hardware": "AME Smile 2.0 board", @@ -51,43 +11,88 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Smile Anna", - "vendor": "Plugwise", - "binary_sensors": { - "plugwise_notification": false - }, "sensors": { "outdoor_temperature": 28.2 - } + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": true, + "cooling_enabled": true, + "cooling_state": true, + "dhw_state": false, + "flame_state": false, + "heating_state": false, + "slave_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 41.5, + "intended_boiler_temperature": 0.0, + "modulation_level": 40, + "outdoor_air_temperature": 28.0, + "return_temperature": 23.8, + "water_pressure": 1.57, + "water_temperature": 22.7 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" }, "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", + "last_used": "standaard", "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "mode": "auto", "model": "ThermoTouch", "name": "Anna", - "vendor": "Plugwise", - "thermostat": { - "setpoint_low": 20.5, - "setpoint_high": 24.0, - "lower_bound": 4.0, - "upper_bound": 30.0, - "resolution": 0.1 - }, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], - "active_preset": "home", - "available_schedules": ["standaard"], - "selected_schedule": "standaard", - "last_used": "standaard", - "mode": "auto", + "select_schedule": "standaard", "sensors": { - "temperature": 26.3, - "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 24.0, "setpoint_low": 20.5, - "setpoint_high": 24.0 - } + "temperature": 26.3 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 24.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" } + }, + "gateway": { + "cooling_present": true, + "gateway_id": "015ae9ea3f964e668e490fa39da3870b", + "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", + "notifications": {}, + "smile_name": "Smile Anna" } -] +} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index 0a421be5343..3a7bd2dae89 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -1,48 +1,9 @@ -[ - { - "smile_name": "Smile Anna", - "gateway_id": "015ae9ea3f964e668e490fa39da3870b", - "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", - "cooling_present": true, - "notifications": {} - }, - { - "1cbf783bb11e4a7c8a6843dee3a86927": { - "dev_class": "heater_central", - "location": "a57efe5f145f498c9be62a9b63626fbf", - "model": "Generic heater/cooler", - "name": "OpenTherm", - "vendor": "Techneco", - "maximum_boiler_temperature": { - "setpoint": 60.0, - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 1.0 - }, - "available": true, - "binary_sensors": { - "cooling_enabled": true, - "dhw_state": false, - "heating_state": false, - "compressor_state": false, - "cooling_state": false, - "slave_boiler_state": false, - "flame_state": false - }, - "sensors": { - "water_temperature": 19.1, - "dhw_temperature": 46.3, - "intended_boiler_temperature": 18.0, - "modulation_level": 0, - "return_temperature": 22.0, - "water_pressure": 1.57, - "outdoor_air_temperature": 28.2 - }, - "switches": { - "dhw_cm_switch": false - } - }, +{ + "devices": { "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "4.0.15", "hardware": "AME Smile 2.0 board", @@ -50,43 +11,88 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Smile Anna", - "vendor": "Plugwise", - "binary_sensors": { - "plugwise_notification": false - }, "sensors": { "outdoor_temperature": 28.2 - } + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": false, + "cooling_enabled": true, + "cooling_state": false, + "dhw_state": false, + "flame_state": false, + "heating_state": false, + "slave_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 46.3, + "intended_boiler_temperature": 18.0, + "modulation_level": 0, + "outdoor_air_temperature": 28.2, + "return_temperature": 22.0, + "water_pressure": 1.57, + "water_temperature": 19.1 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" }, "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", + "last_used": "standaard", "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "mode": "auto", "model": "ThermoTouch", "name": "Anna", - "vendor": "Plugwise", - "thermostat": { - "setpoint_low": 20.5, - "setpoint_high": 24.0, - "lower_bound": 4.0, - "upper_bound": 30.0, - "resolution": 0.1 - }, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], - "active_preset": "home", - "available_schedules": ["standaard"], - "selected_schedule": "standaard", - "last_used": "standaard", - "mode": "auto", + "select_schedule": "standaard", "sensors": { - "temperature": 23.0, - "illuminance": 86.0, "cooling_activation_outdoor_temperature": 25.0, "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 24.0, "setpoint_low": 20.5, - "setpoint_high": 24.0 - } + "temperature": 23.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 24.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" } + }, + "gateway": { + "cooling_present": true, + "gateway_id": "015ae9ea3f964e668e490fa39da3870b", + "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", + "notifications": {}, + "smile_name": "Smile Anna" } -] +} diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json index c52f33e6323..0e0b3c51a07 100644 --- a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json +++ b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json @@ -1,11 +1,9 @@ -[ - { - "smile_name": "Smile P1", - "gateway_id": "cd3e822288064775a7c4afcdd70bdda2", - "notifications": {} - }, - { +{ + "devices": { "cd3e822288064775a7c4afcdd70bdda2": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "3.3.9", "hardware": "AME Smile 2.0 board", @@ -13,36 +11,38 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Smile P1", - "vendor": "Plugwise", - "binary_sensors": { - "plugwise_notification": false - } + "vendor": "Plugwise" }, "e950c7d5e1ee407a858e2a8b5016c8b3": { + "available": true, "dev_class": "smartmeter", "location": "cd3e822288064775a7c4afcdd70bdda2", "model": "2M550E-1012", "name": "P1", - "vendor": "ISKRAEMECO", - "available": true, "sensors": { - "net_electricity_point": -2816, - "electricity_consumed_peak_point": 0, - "electricity_consumed_off_peak_point": 0, - "net_electricity_cumulative": 442.972, - "electricity_consumed_peak_cumulative": 442.932, "electricity_consumed_off_peak_cumulative": 551.09, - "electricity_consumed_peak_interval": 0, "electricity_consumed_off_peak_interval": 0, - "electricity_produced_peak_point": 2816, + "electricity_consumed_off_peak_point": 0, + "electricity_consumed_peak_cumulative": 442.932, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_produced_off_peak_cumulative": 154.491, + "electricity_produced_off_peak_interval": 0, "electricity_produced_off_peak_point": 0, "electricity_produced_peak_cumulative": 396.559, - "electricity_produced_off_peak_cumulative": 154.491, "electricity_produced_peak_interval": 0, - "electricity_produced_off_peak_interval": 0, + "electricity_produced_peak_point": 2816, "gas_consumed_cumulative": 584.85, - "gas_consumed_interval": 0.0 - } + "gas_consumed_interval": 0.0, + "net_electricity_cumulative": 442.972, + "net_electricity_point": -2816 + }, + "vendor": "ISKRAEMECO" } + }, + "gateway": { + "gateway_id": "cd3e822288064775a7c4afcdd70bdda2", + "notifications": {}, + "smile_name": "Smile P1" } -] +} diff --git a/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json similarity index 88% rename from tests/components/plugwise/fixtures/p1v4_3ph/all_data.json rename to tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json index 852ca2857cd..e9a3b4c68b9 100644 --- a/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json @@ -1,11 +1,9 @@ -[ - { - "smile_name": "Smile P1", - "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", - "notifications": {} - }, - { +{ + "devices": { "03e65b16e4b247a29ae0d75a78cb492e": { + "binary_sensors": { + "plugwise_notification": false + }, "dev_class": "gateway", "firmware": "4.4.2", "hardware": "AME Smile 2.0 board", @@ -13,45 +11,47 @@ "mac_address": "012345670001", "model": "Gateway", "name": "Smile P1", - "vendor": "Plugwise", - "binary_sensors": { - "plugwise_notification": false - } + "vendor": "Plugwise" }, "b82b6b3322484f2ea4e25e0bd5f3d61f": { + "available": true, "dev_class": "smartmeter", "location": "03e65b16e4b247a29ae0d75a78cb492e", "model": "XMX5LGF0010453051839", "name": "P1", - "vendor": "XEMEX NV", - "available": true, "sensors": { - "net_electricity_point": 5553, - "electricity_consumed_peak_point": 0, - "electricity_consumed_off_peak_point": 5553, - "net_electricity_cumulative": 231866.539, - "electricity_consumed_peak_cumulative": 161328.641, "electricity_consumed_off_peak_cumulative": 70537.898, - "electricity_consumed_peak_interval": 0, "electricity_consumed_off_peak_interval": 314, - "electricity_produced_peak_point": 0, + "electricity_consumed_off_peak_point": 5553, + "electricity_consumed_peak_cumulative": 161328.641, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_phase_one_consumed": 1763, + "electricity_phase_one_produced": 0, + "electricity_phase_three_consumed": 2080, + "electricity_phase_three_produced": 0, + "electricity_phase_two_consumed": 1703, + "electricity_phase_two_produced": 0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_off_peak_interval": 0, "electricity_produced_off_peak_point": 0, "electricity_produced_peak_cumulative": 0.0, - "electricity_produced_off_peak_cumulative": 0.0, "electricity_produced_peak_interval": 0, - "electricity_produced_off_peak_interval": 0, - "electricity_phase_one_consumed": 1763, - "electricity_phase_two_consumed": 1703, - "electricity_phase_three_consumed": 2080, - "electricity_phase_one_produced": 0, - "electricity_phase_two_produced": 0, - "electricity_phase_three_produced": 0, + "electricity_produced_peak_point": 0, "gas_consumed_cumulative": 16811.37, "gas_consumed_interval": 0.06, + "net_electricity_cumulative": 231866.539, + "net_electricity_point": 5553, "voltage_phase_one": 233.2, - "voltage_phase_two": 234.4, - "voltage_phase_three": 234.7 - } + "voltage_phase_three": 234.7, + "voltage_phase_two": 234.4 + }, + "vendor": "XEMEX NV" } + }, + "gateway": { + "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", + "notifications": {}, + "smile_name": "Smile P1" } -] +} diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json b/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index 1ce34e376d7..c336a9cb9c2 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -1,10 +1,5 @@ -[ - { - "smile_name": "Stretch", - "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", - "notifications": {} - }, - { +{ + "devices": { "0000aaaa0000aaaa0000aaaa0000aa00": { "dev_class": "gateway", "firmware": "3.1.11", @@ -12,8 +7,27 @@ "mac_address": "01:23:45:67:89:AB", "model": "Gateway", "name": "Stretch", - "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise" + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + }, + "059e4d03c7a34d278add5c7a4a781d19": { + "dev_class": "washingmachine", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Wasmachine (52AC1)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" }, "5871317346d045bc9f6b987ef25ee638": { "dev_class": "water_heater_vessel", @@ -22,35 +36,25 @@ "location": "0000aaaa0000aaaa0000aaaa0000aa00", "model": "Circle type F", "name": "Boiler (1EB31)", - "zigbee_mac_address": "ABCD012345670A07", - "vendor": "Plugwise", "sensors": { "electricity_consumed": 1.19, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0 }, "switches": { - "relay": true, - "lock": false - } - }, - "e1c884e7dede431dadee09506ec4f859": { - "dev_class": "refrigerator", - "firmware": "2011-06-27T10:47:37+02:00", - "hardware": "6539-0700-7330", - "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "model": "Circle+ type F", - "name": "Koelkast (92C4A)", - "zigbee_mac_address": "0123456789AB", - "vendor": "Plugwise", - "sensors": { - "electricity_consumed": 50.5, - "electricity_consumed_interval": 0.08, - "electricity_produced": 0.0 + "lock": false, + "relay": true }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "71e1944f2a944b26ad73323e399efef0": { + "dev_class": "switching", + "members": ["5ca521ac179d468e91d772eeeb8a2117"], + "model": "Switchgroup", + "name": "Test", "switches": { - "relay": true, - "lock": false + "relay": true } }, "aac7b735042c4832ac9ff33aae4f453b": { @@ -60,17 +64,17 @@ "location": "0000aaaa0000aaaa0000aaaa0000aa00", "model": "Circle type F", "name": "Vaatwasser (2a1ab)", - "zigbee_mac_address": "ABCD012345670A02", - "vendor": "Plugwise", "sensors": { "electricity_consumed": 0.0, "electricity_consumed_interval": 0.71, "electricity_produced": 0.0 }, "switches": { - "relay": true, - "lock": false - } + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" }, "cfe95cf3de1948c0b8955125bf754614": { "dev_class": "dryer", @@ -79,50 +83,32 @@ "location": "0000aaaa0000aaaa0000aaaa0000aa00", "model": "Circle type F", "name": "Droger (52559)", - "zigbee_mac_address": "ABCD012345670A04", - "vendor": "Plugwise", "sensors": { "electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0 }, "switches": { - "relay": true, - "lock": false - } - }, - "059e4d03c7a34d278add5c7a4a781d19": { - "dev_class": "washingmachine", - "firmware": "2011-06-27T10:52:18+02:00", - "hardware": "0000-0440-0107", - "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "model": "Circle type F", - "name": "Wasmachine (52AC1)", - "zigbee_mac_address": "ABCD012345670A01", - "vendor": "Plugwise", - "sensors": { - "electricity_consumed": 0.0, - "electricity_consumed_interval": 0.0, - "electricity_produced": 0.0 + "lock": false, + "relay": true }, - "switches": { - "relay": true, - "lock": false - } + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" }, - "71e1944f2a944b26ad73323e399efef0": { + "d03738edfcc947f7b8f4573571d90d2d": { "dev_class": "switching", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "cfe95cf3de1948c0b8955125bf754614" + ], "model": "Switchgroup", - "name": "Test", - "members": ["5ca521ac179d468e91d772eeeb8a2117"], + "name": "Schakel", "switches": { "relay": true } }, "d950b314e9d8499f968e6db8d82ef78c": { "dev_class": "report", - "model": "Switchgroup", - "name": "Stroomvreters", "members": [ "059e4d03c7a34d278add5c7a4a781d19", "5871317346d045bc9f6b987ef25ee638", @@ -130,21 +116,35 @@ "cfe95cf3de1948c0b8955125bf754614", "e1c884e7dede431dadee09506ec4f859" ], + "model": "Switchgroup", + "name": "Stroomvreters", "switches": { "relay": true } }, - "d03738edfcc947f7b8f4573571d90d2d": { - "dev_class": "switching", - "model": "Switchgroup", - "name": "Schakel", - "members": [ - "059e4d03c7a34d278add5c7a4a781d19", - "cfe95cf3de1948c0b8955125bf754614" - ], + "e1c884e7dede431dadee09506ec4f859": { + "dev_class": "refrigerator", + "firmware": "2011-06-27T10:47:37+02:00", + "hardware": "6539-0700-7330", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle+ type F", + "name": "Koelkast (92C4A)", + "sensors": { + "electricity_consumed": 50.5, + "electricity_consumed_interval": 0.08, + "electricity_produced": 0.0 + }, "switches": { + "lock": false, "relay": true - } + }, + "vendor": "Plugwise", + "zigbee_mac_address": "0123456789AB" } + }, + "gateway": { + "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", + "notifications": {}, + "smile_name": "Stretch" } -] +} diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index f4f2a3f3c5f..aec20bc4a0b 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry @@ -30,6 +29,10 @@ async def test_anna_climate_binary_sensor_entities( assert state assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.opentherm_compressor_state") + assert state + assert state.state == STATE_ON + async def test_anna_climate_binary_sensor_change( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry @@ -42,7 +45,9 @@ async def test_anna_climate_binary_sensor_change( assert state assert state.state == STATE_ON - await async_update_entity(hass, "binary_sensor.opentherm_dhw_state") + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.opentherm_dhw_state" + ) state = hass.states.get("binary_sensor.opentherm_dhw_state") assert state diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 5636523a919..c73bd5b6190 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -135,6 +135,22 @@ async def test_adam_climate_entity_climate_changes( "c50f167537524366a5af7aa3942feb1e", {"setpoint": 25.0} ) + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.zone_lisa_wk", + "hvac_mode": "heat", + "temperature": 25, + }, + blocking=True, + ) + + assert mock_smile_adam.set_temperature.call_count == 2 + mock_smile_adam.set_temperature.assert_called_with( + "c50f167537524366a5af7aa3942feb1e", {"setpoint": 25.0} + ) + with pytest.raises(ValueError): await hass.services.async_call( "climate", @@ -162,7 +178,7 @@ async def test_adam_climate_entity_climate_changes( blocking=True, ) - assert mock_smile_adam.set_temperature.call_count == 2 + assert mock_smile_adam.set_temperature.call_count == 3 mock_smile_adam.set_temperature.assert_called_with( "82fa13f017d240daa0d0ea1775420f24", {"setpoint": 25.0} ) diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index 6f73619ea77..5dde8a0e09e 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -15,6 +15,7 @@ async def test_diagnostics( init_integration: MockConfigEntry, ) -> None: """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { @@ -55,7 +56,7 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "selected_schedule": "None", + "select_schedule": "None", "last_used": "Badkamer Schema", "mode": "heat", "sensors": {"temperature": 16.5, "setpoint": 13.0, "battery": 67}, @@ -120,7 +121,7 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "selected_schedule": "GF7 Woonkamer", + "select_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer", "mode": "auto", "sensors": {"temperature": 20.9, "setpoint": 21.5, "battery": 34}, @@ -135,7 +136,7 @@ async def test_diagnostics( "name": "Adam", "zigbee_mac_address": "ABCD012345670101", "vendor": "Plugwise", - "regulation_mode": "heating", + "select_regulation_mode": "heating", "binary_sensors": {"plugwise_notification": True}, "sensors": {"outdoor_temperature": 7.81}, }, @@ -296,7 +297,7 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "selected_schedule": "CV Jessie", + "select_schedule": "CV Jessie", "last_used": "CV Jessie", "mode": "auto", "sensors": {"temperature": 17.2, "setpoint": 15.0, "battery": 37}, @@ -344,7 +345,7 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "selected_schedule": "Badkamer Schema", + "select_schedule": "Badkamer Schema", "last_used": "Badkamer Schema", "mode": "auto", "sensors": {"temperature": 18.9, "setpoint": 14.0, "battery": 92}, @@ -391,7 +392,7 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "selected_schedule": "None", + "select_schedule": "None", "last_used": "Badkamer Schema", "mode": "heat", "sensors": { diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 1ed1e509cef..1b5297b71d2 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -99,7 +99,7 @@ async def test_migrate_unique_id_temperature( mock_config_entry.add_to_hass(hass) entity_registry = er.async_get(hass) - entity: er.RegistryEntry = entity_registry.async_get_or_create( + entity: entity_registry.RegistryEntry = entity_registry.async_get_or_create( **entitydata, config_entry=mock_config_entry, ) @@ -140,7 +140,7 @@ async def test_migrate_unique_id_relay( mock_config_entry.add_to_hass(hass) entity_registry = er.async_get(hass) - entity: er.RegistryEntry = entity_registry.async_get_or_create( + entity: entity_registry.RegistryEntry = entity_registry.async_get_or_create( **entitydata, config_entry=mock_config_entry, ) diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 0c7483c19bd..46f31e1458f 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock +from homeassistant.components.plugwise.const import DOMAIN +from homeassistant.components.plugwise.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_registry import async_get @@ -36,6 +38,58 @@ async def test_adam_climate_sensor_entities( assert int(state.state) == 34 +async def test_adam_climate_sensor_entity_2( + hass: HomeAssistant, mock_smile_adam_4: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of climate related sensor entities.""" + state = hass.states.get("sensor.woonkamer_humidity") + assert state + assert float(state.state) == 56.2 + + +async def test_unique_id_migration_humidity( + hass: HomeAssistant, + mock_smile_adam_4: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unique ID migration of -relative_humidity to -humidity.""" + mock_config_entry.add_to_hass(hass) + + entity_registry = async_get(hass) + # Entry to migrate + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "f61f1a2535f54f52ad006a3d18e459ca-relative_humidity", + config_entry=mock_config_entry, + suggested_object_id="woonkamer_humidity", + disabled_by=None, + ) + # Entry not needing migration + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "f61f1a2535f54f52ad006a3d18e459ca-battery", + config_entry=mock_config_entry, + suggested_object_id="woonkamer_battery", + disabled_by=None, + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.woonkamer_humidity") is not None + assert hass.states.get("sensor.woonkamer_battery") is not None + + entity_entry = entity_registry.async_get("sensor.woonkamer_humidity") + assert entity_entry + assert entity_entry.unique_id == "f61f1a2535f54f52ad006a3d18e459ca-humidity" + + entity_entry = entity_registry.async_get("sensor.woonkamer_battery") + assert entity_entry + assert entity_entry.unique_id == "f61f1a2535f54f52ad006a3d18e459ca-battery" + + async def test_anna_as_smt_climate_sensor_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -48,10 +102,6 @@ async def test_anna_as_smt_climate_sensor_entities( assert state assert float(state.state) == 29.1 - state = hass.states.get("sensor.opentherm_dhw_setpoint") - assert state - assert float(state.state) == 60.0 - state = hass.states.get("sensor.opentherm_dhw_temperature") assert state assert float(state.state) == 46.3 diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index 64519aba0a8..2d47a420fe8 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -173,7 +173,7 @@ async def test_unique_id_migration_plug_relay( DOMAIN, "675416a629f343c495449970e2ca37b5-relay", config_entry=mock_config_entry, - suggested_object_id="router", + suggested_object_id="ziggo_modem", disabled_by=None, ) @@ -181,12 +181,12 @@ async def test_unique_id_migration_plug_relay( await hass.async_block_till_done() assert hass.states.get("switch.playstation_smart_plug") is not None - assert hass.states.get("switch.router") is not None + assert hass.states.get("switch.ziggo_modem") is not None entity_entry = registry.async_get("switch.playstation_smart_plug") assert entity_entry assert entity_entry.unique_id == "21f2b542c49845e6bb416884c55778d6-relay" - entity_entry = registry.async_get("switch.router") + entity_entry = registry.async_get("switch.ziggo_modem") assert entity_entry assert entity_entry.unique_id == "675416a629f343c495449970e2ca37b5-relay" From fc463e5831570adeb0898aac2030bbbdb7ca7df9 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 8 Aug 2023 16:40:16 +0200 Subject: [PATCH 052/179] modbus: remove unused constants and get 100% coverage. (#97779) --- homeassistant/components/modbus/__init__.py | 2 - homeassistant/components/modbus/const.py | 12 +----- tests/components/modbus/test_init.py | 17 +++++++++ tests/components/modbus/test_sensor.py | 41 +++++++++++++-------- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 0108c37a10b..920188603fc 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -55,7 +55,6 @@ from .const import ( # noqa: F401 CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, - CALL_TYPE_WRITE_REGISTER, CALL_TYPE_X_COILS, CALL_TYPE_X_REGISTER_HOLDINGS, CONF_BAUDRATE, @@ -64,7 +63,6 @@ from .const import ( # noqa: F401 CONF_CLOSE_COMM_ON_ERROR, CONF_DATA_TYPE, CONF_FANS, - CONF_HUB, CONF_HVAC_MODE_AUTO, CONF_HVAC_MODE_COOL, CONF_HVAC_MODE_DRY, diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 3b565e91f92..e509577267c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -16,13 +16,8 @@ CONF_BAUDRATE = "baudrate" CONF_BYTESIZE = "bytesize" CONF_CLIMATES = "climates" CONF_CLOSE_COMM_ON_ERROR = "close_comm_on_error" -CONF_COILS = "coils" -CONF_CURRENT_TEMP = "current_temp_register" -CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" CONF_DATA_TYPE = "data_type" CONF_FANS = "fans" -CONF_HUB = "hub" -CONF_INPUTS = "inputs" CONF_INPUT_TYPE = "input_type" CONF_LAZY_ERROR = "lazy_error_count" CONF_MAX_TEMP = "max_temp" @@ -32,9 +27,6 @@ CONF_MIN_VALUE = "min_value" CONF_MSG_WAIT = "message_wait_milliseconds" CONF_NAN_VALUE = "nan_value" CONF_PARITY = "parity" -CONF_REGISTER = "register" -CONF_REGISTER_TYPE = "register_type" -CONF_REGISTERS = "registers" CONF_RETRIES = "retries" CONF_RETRY_ON_EMPTY = "retry_on_empty" CONF_PRECISION = "precision" @@ -69,8 +61,6 @@ CONF_HVAC_MODE_DRY = "state_dry" CONF_HVAC_MODE_FAN_ONLY = "state_fan_only" CONF_WRITE_REGISTERS = "write_registers" CONF_VERIFY = "verify" -CONF_VERIFY_REGISTER = "verify_register" -CONF_VERIFY_STATE = "verify_state" CONF_WRITE_TYPE = "write_type" CONF_ZERO_SUPPRESS = "zero_suppress" @@ -82,7 +72,7 @@ UDP = "udp" # service call attributes ATTR_ADDRESS = CONF_ADDRESS -ATTR_HUB = CONF_HUB +ATTR_HUB = "hub" ATTR_UNIT = "unit" ATTR_SLAVE = "slave" ATTR_VALUE = "value" diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index d9d3b035c94..35c01ec478b 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -929,3 +929,20 @@ async def test_integration_reload_failed( assert "Modbus reloading" in caplog.text assert "connect failed, retry in pymodbus" in caplog.text + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_integration_setup_failed( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus +) -> None: + """Run test for integration setup on reload.""" + with mock.patch.object( + hass_config, + "YAML_CONFIG_FILE", + get_fixture_path("configuration.yaml", "modbus"), + ): + hass.data[DOMAIN][TEST_MODBUS_NAME].async_setup = mock.AsyncMock( + return_value=False + ) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.async_block_till_done() diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 48a081ef637..06b0b68a746 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -47,6 +47,8 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle +from tests.common import mock_restore_cache_with_extra_data + ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") SLAVE_UNIQUE_ID = "ground_floor_sensor" @@ -906,23 +908,27 @@ async def test_wrap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None assert hass.states.get(ENTITY_ID).state == expected -@pytest.mark.parametrize( - "mock_test_state", - [(State(ENTITY_ID, "unknown"), State(f"{ENTITY_ID}_1", "119"))], - indirect=True, -) +@pytest.fixture(name="mock_restore") +async def mock_restore(hass): + """Mock restore cache.""" + mock_restore_cache_with_extra_data( + hass, + ( + ( + State(ENTITY_ID, "121"), + {"native_value": "121", "native_unit_of_measurement": "kg"}, + ), + ( + State(ENTITY_ID + "_1", "119"), + {"native_value": "119", "native_unit_of_measurement": "kg"}, + ), + ), + ) + + @pytest.mark.parametrize( "do_config", [ - { - CONF_SENSORS: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_ADDRESS: 51, - CONF_SCAN_INTERVAL: 0, - } - ] - }, { CONF_SENSORS: [ { @@ -936,10 +942,13 @@ async def test_wrap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None ], ) async def test_restore_state_sensor( - hass: HomeAssistant, mock_test_state, mock_modbus + hass: HomeAssistant, mock_restore, mock_modbus ) -> None: """Run test for sensor restore state.""" - assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state + state = hass.states.get(ENTITY_ID).state + state2 = hass.states.get(ENTITY_ID + "_1").state + assert state + assert state2 @pytest.mark.parametrize( From 9910da2f3de41f2b7676330e095c7635fcdbe18a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Aug 2023 14:47:18 +0000 Subject: [PATCH 053/179] Add `neutral current` sensor for Shelly 3EM (#97981) --- homeassistant/components/shelly/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b52e176b521..896ffd72327 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -97,6 +97,14 @@ SENSORS: Final = { device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), + ("device", "neutralCurrent"): BlockSensorDescription( + key="device|neutralCurrent", + name="Neutral current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), ("light", "power"): BlockSensorDescription( key="light|power", name="Power", From 500d9a4da00008ff54435ebc11466cf1abdfa916 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Aug 2023 17:15:25 +0200 Subject: [PATCH 054/179] Alexa strict type hints (#97485) * Enable strict typing * Adjustments for stict typing --- .strict-typing | 1 + homeassistant/components/alexa/auth.py | 3 ++- homeassistant/components/alexa/capabilities.py | 2 +- homeassistant/components/alexa/config.py | 2 +- homeassistant/components/alexa/entities.py | 9 +++++---- homeassistant/components/alexa/handlers.py | 4 +++- homeassistant/components/alexa/intent.py | 5 +++-- homeassistant/components/alexa/smart_home.py | 2 +- homeassistant/components/alexa/state_report.py | 17 ++++++++++------- mypy.ini | 10 ++++++++++ 10 files changed, 37 insertions(+), 18 deletions(-) diff --git a/.strict-typing b/.strict-typing index eec8bd906fe..c56c7d9f137 100644 --- a/.strict-typing +++ b/.strict-typing @@ -53,6 +53,7 @@ homeassistant.components.airzone_cloud.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.alert.* +homeassistant.components.alexa.* homeassistant.components.amazon_polly.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index ea237e4c92c..61a87d9ebab 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -76,7 +76,8 @@ class Auth: assert self._prefs is not None if self.is_token_valid(): _LOGGER.debug("Token still valid, using it") - return self._prefs[STORAGE_ACCESS_TOKEN] + token: str = self._prefs[STORAGE_ACCESS_TOKEN] + return token if self._prefs[STORAGE_REFRESH_TOKEN] is None: _LOGGER.debug("Token invalid and no refresh token available") diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 042ddfac3d5..a7065a38686 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1990,7 +1990,7 @@ class AlexaDoorbellEventSource(AlexaCapability): """Return the Alexa API name of this interface.""" return "Alexa.DoorbellEventSource" - def capability_proactively_reported(self): + def capability_proactively_reported(self) -> bool: """Return True for proactively reported capability.""" return True diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 8c9965662bc..a1ab1d77081 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -145,7 +145,7 @@ class AlexaConfigStore: def authorized(self) -> bool: """Return authorization status.""" assert self._data is not None - return self._data[STORE_AUTHORIZED] + return bool(self._data[STORE_AUTHORIZED]) @callback def set_authorized(self, authorized: bool) -> None: diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 2931326d430..7f6331515c6 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -280,9 +280,10 @@ class AlexaEntity: def friendly_name(self) -> str: """Return the Alexa API friendly name.""" - return self.entity_conf.get(CONF_NAME, self.entity.name).translate( - TRANSLATION_TABLE - ) + friendly_name: str = self.entity_conf.get( + CONF_NAME, self.entity.name + ).translate(TRANSLATION_TABLE) + return friendly_name def description(self) -> str: """Return the Alexa API description.""" @@ -725,7 +726,7 @@ class MediaPlayerCapabilities(AlexaEntity): class SceneCapabilities(AlexaEntity): """Class to represent Scene capabilities.""" - def description(self): + def description(self) -> str: """Return the Alexa API description.""" description = AlexaEntity.description(self) if "scene" not in description.casefold(): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index a37c8b64ab8..06ce4f88b56 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -758,7 +758,9 @@ async def async_api_previous( return directive.response() -def temperature_from_object(hass: ha.HomeAssistant, temp_obj, interval=False): +def temperature_from_object( + hass: ha.HomeAssistant, temp_obj: dict[str, Any], interval: bool = False +) -> float: """Get temperature from Temperature object in requested unit.""" to_unit = hass.config.units.temperature_unit from_unit = UnitOfTemperature.CELSIUS diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index ad950803f5c..58319dd44b5 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -129,7 +129,8 @@ async def async_handle_message( if not (handler := HANDLERS.get(req_type)): raise UnknownRequest(f"Received unknown request {req_type}") - return await handler(hass, message) + response: dict[str, Any] = await handler(hass, message) + return response @HANDLERS.register("SessionEndedRequest") @@ -282,7 +283,7 @@ class AlexaIntentResponse: self.speech = {"type": speech_type.value, key: text} - def add_reprompt(self, speech_type: SpeechType, text) -> None: + def add_reprompt(self, speech_type: SpeechType, text: str) -> None: """Add reprompt if user does not answer.""" assert self.reprompt is None diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 288f6adcc15..a8101896116 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -82,7 +82,7 @@ class AlexaConfig(AbstractConfig): def should_expose(self, entity_id: str) -> bool: """If an entity should be exposed.""" if not self._config[CONF_FILTER].empty_filter: - return self._config[CONF_FILTER](entity_id) + return bool(self._config[CONF_FILTER](entity_id)) entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 4e3c33386ca..bbaa8a240f7 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -14,7 +14,7 @@ import async_timeout from homeassistant.components import event from homeassistant.const import MATCH_ALL, STATE_ON -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.significant_change import create_checker @@ -159,12 +159,14 @@ class AlexaResponse: @property def name(self) -> str: """Return the name of this response.""" - return self._response[API_EVENT][API_HEADER]["name"] + name: str = self._response[API_EVENT][API_HEADER]["name"] + return name @property def namespace(self) -> str: """Return the namespace of this response.""" - return self._response[API_EVENT][API_HEADER]["namespace"] + namespace: str = self._response[API_EVENT][API_HEADER]["namespace"] + return namespace def set_correlation_token(self, token: str) -> None: """Set the correlationToken. @@ -196,9 +198,10 @@ class AlexaResponse: """ self._response[API_EVENT][API_ENDPOINT] = endpoint - def _properties(self): - context = self._response.setdefault(API_CONTEXT, {}) - return context.setdefault("properties", []) + def _properties(self) -> list[dict[str, Any]]: + context: dict[str, Any] = self._response.setdefault(API_CONTEXT, {}) + properties: list[dict[str, Any]] = context.setdefault("properties", []) + return properties def add_context_property(self, prop: dict[str, Any]) -> None: """Add a property to the response context. @@ -236,7 +239,7 @@ class AlexaResponse: async def async_enable_proactive_mode( hass: HomeAssistant, smart_home_config: AbstractConfig -): +) -> CALLBACK_TYPE | None: """Enable the proactive mode. Proactive mode makes this component report state changes to Alexa. diff --git a/mypy.ini b/mypy.ini index 639f27bbabb..b3ab53bf8a9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -291,6 +291,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.alexa.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.amazon_polly.*] check_untyped_defs = true disallow_incomplete_defs = true From ce1077934aeee92cc1d096394e9932abcc82f667 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 8 Aug 2023 17:38:38 +0200 Subject: [PATCH 055/179] Move all used modbus constants to Stiebel (#98044) --- homeassistant/components/stiebel_eltron/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index 84a39e3c875..13ca12f482e 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -5,11 +5,6 @@ import logging from pystiebeleltron import pystiebeleltron import voluptuous as vol -from homeassistant.components.modbus import ( - CONF_HUB, - DEFAULT_HUB, - DOMAIN as MODBUS_DOMAIN, -) from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery @@ -17,6 +12,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle +CONF_HUB = "hub" +DEFAULT_HUB = "modbus_hub" +MODBUS_DOMAIN = "modbus" DOMAIN = "stiebel_eltron" CONFIG_SCHEMA = vol.Schema( From c78c2b7c3b908296e5eb142b944185e3cdbf508c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 8 Aug 2023 18:02:47 +0200 Subject: [PATCH 056/179] Add translation keys to Tuya cover (#98040) --- homeassistant/components/tuya/cover.py | 5 +++++ homeassistant/components/tuya/strings.json | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 3505bbf9f22..da9f7d29eb2 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -44,6 +44,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { "cl": ( TuyaCoverEntityDescription( key=DPCode.CONTROL, + translation_key="curtain", current_state=DPCode.SITUATION_SET, current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE), set_position=DPCode.PERCENT_CONTROL, @@ -65,6 +66,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { ), TuyaCoverEntityDescription( key=DPCode.MACH_OPERATE, + translation_key="curtain", current_position=DPCode.POSITION, set_position=DPCode.POSITION, device_class=CoverDeviceClass.CURTAIN, @@ -76,6 +78,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { # It is used by the Kogan Smart Blinds Driver TuyaCoverEntityDescription( key=DPCode.SWITCH_1, + translation_key="blind", current_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.BLIND, @@ -111,6 +114,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { "clkg": ( TuyaCoverEntityDescription( key=DPCode.CONTROL, + translation_key="curtain", current_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.CURTAIN, @@ -128,6 +132,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { "jdcljqr": ( TuyaCoverEntityDescription( key=DPCode.CONTROL, + translation_key="curtain", current_position=DPCode.PERCENT_STATE, set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.CURTAIN, diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index db16015ba56..1ea58f5029f 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -71,6 +71,12 @@ } }, "cover": { + "blind": { + "name": "[%key:component::cover::entity_component::blind::name%]" + }, + "curtain": { + "name": "[%key:component::cover::entity_component::curtain::name%]" + }, "curtain_2": { "name": "Curtain 2" }, From 466c5ce591366b2d5bb4aa1718bb9e125b8c31d0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 8 Aug 2023 18:41:47 +0200 Subject: [PATCH 057/179] Add some constants back that were used to Flexit and Stiebel (#98042) * Add some constants back that were used * Update __init__.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/flexit/climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index dbe1e060a12..b833617f2ca 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -16,8 +16,6 @@ from homeassistant.components.climate import ( from homeassistant.components.modbus import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, - CALL_TYPE_WRITE_REGISTER, - CONF_HUB, DEFAULT_HUB, ModbusHub, get_hub, @@ -34,6 +32,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +CALL_TYPE_WRITE_REGISTER = "write_register" +CONF_HUB = "hub" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, From eb64e89ecf242e18d13d0d474539eb39f28daacd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 18:49:56 +0200 Subject: [PATCH 058/179] Make changes in modbus trigger a full CI run (#98055) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 5e9b1d50def..4ac65cd92c7 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -86,6 +86,7 @@ components: &components - homeassistant/components/lovelace/** - homeassistant/components/media_source/** - homeassistant/components/mjpeg/** + - homeassistant/components/modbus/** - homeassistant/components/mqtt/** - homeassistant/components/network/** - homeassistant/components/onboarding/** From 14a993d33b951972c831da1d8f7793d745a1541c Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 8 Aug 2023 18:07:17 +0100 Subject: [PATCH 059/179] Remove trailing . from melcloud service descriptions (#98053) --- homeassistant/components/melcloud/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index 13827e4e5b5..23c1c63d328 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -26,7 +26,7 @@ "fields": { "position": { "name": "Position", - "description": "Horizontal vane position. Possible options can be found in the vane_horizontal_positions state attribute.\n." + "description": "Horizontal vane position. Possible options can be found in the vane_horizontal_positions state attribute." } } }, @@ -36,7 +36,7 @@ "fields": { "position": { "name": "Position", - "description": "Vertical vane position. Possible options can be found in the vane_vertical_positions state attribute.\n." + "description": "Vertical vane position. Possible options can be found in the vane_vertical_positions state attribute." } } } From 75fbc7a97ca639463f6204cdf53386152d938b7e Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 8 Aug 2023 18:07:40 +0100 Subject: [PATCH 060/179] Hyphenate "human-readable" in LIFX service description (#98058) --- homeassistant/components/lifx/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index 9d155ae32ae..c327081fabd 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -101,7 +101,7 @@ }, "color_name": { "name": "Color name", - "description": "A human readable color name." + "description": "A human-readable color name." }, "rgb_color": { "name": "RGB color", From f36e75ecf19e83dc06116c101661e284b3b4d76c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 19:11:55 +0200 Subject: [PATCH 061/179] Add WeatherEntity.__post_init__ (#98034) --- homeassistant/components/weather/__init__.py | 33 +++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 7bd897bb638..635c4948285 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 +import abc from collections.abc import Callable, Iterable from contextlib import suppress from dataclasses import dataclass @@ -204,7 +205,25 @@ class WeatherEntityDescription(EntityDescription): """A class that describes weather entities.""" -class WeatherEntity(Entity): +class PostInitMeta(abc.ABCMeta): + """Meta class which calls __post_init__ after __new__ and __init__.""" + + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + """Create an instance.""" + instance: PostInit = super().__call__(*args, **kwargs) + instance.__post_init__(*args, **kwargs) + return instance + + +class PostInit(metaclass=PostInitMeta): + """Class which calls __post_init__ after __new__ and __init__.""" + + @abc.abstractmethod + def __post_init__(self, *args: Any, **kwargs: Any) -> None: + """Finish initializing.""" + + +class WeatherEntity(Entity, PostInit): """ABC for weather data.""" entity_description: WeatherEntityDescription @@ -271,9 +290,14 @@ class WeatherEntity(Entity): _weather_option_precipitation_unit: str | None = None _weather_option_wind_speed_unit: str | None = None + def __post_init__(self, *args: Any, **kwargs: Any) -> None: + """Finish initializing.""" + self._forecast_listeners = {"daily": [], "hourly": [], "twice_daily": []} + def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" super().__init_subclass__(**kwargs) + _reported = False if any( method in cls.__dict__ @@ -326,7 +350,6 @@ class WeatherEntity(Entity): async def async_internal_added_to_hass(self) -> None: """Call when the weather entity is added to hass.""" await super().async_internal_added_to_hass() - self._forecast_listeners = {"daily": [], "hourly": [], "twice_daily": []} if not self.registry_entry: return self.async_registry_entry_updated() @@ -1072,12 +1095,6 @@ class WeatherEntity(Entity): self, forecast_types: Iterable[Literal["daily", "hourly", "twice_daily"]] | None ) -> None: """Push updated forecast to all listeners.""" - if not hasattr(self, "_forecast_listeners"): - # Required for entities initiated with `update_before_add` - # as `self._forecast_listeners` has not yet been set. - # `async_internal_added_to_hass()` will execute once entity has been added. - return - if forecast_types is None: forecast_types = {"daily", "hourly", "twice_daily"} for forecast_type in forecast_types: From bfc578a7573445505982283db962487bdb493331 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 8 Aug 2023 18:13:35 +0100 Subject: [PATCH 062/179] Fix address typo in Reolink SSL issue description (#98060) --- homeassistant/components/reolink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 806f2498094..08ee78fd930 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -46,7 +46,7 @@ }, "ssl": { "title": "Reolink incompatible with global SSL certificate", - "description": "Global SSL certificate configured in the [configuration.yaml under http]({ssl_link}) while a local HTTP address `{base_url}` is configured under \"Home Assistant URL\" in the [network settings]({network_link}). Therefore the Reolink device can not reach Home Assistant to push its motion/AI events. Please make sure the local HTTP adress is not covered by the SSL certificate, by for instance using [NGINX add-on]({nginx_link}) instead of a globally enforced SSL certificate." + "description": "Global SSL certificate configured in the [configuration.yaml under http]({ssl_link}) while a local HTTP address `{base_url}` is configured under \"Home Assistant URL\" in the [network settings]({network_link}). Therefore, the Reolink device can not reach Home Assistant to push its motion/AI events. Please make sure the local HTTP address is not covered by the SSL certificate, by for instance using [NGINX add-on]({nginx_link}) instead of a globally enforced SSL certificate." }, "webhook_url": { "title": "Reolink webhook URL unreachable", From d557f3b742b7a019121de12f925b34ef7da40389 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 8 Aug 2023 19:13:56 +0200 Subject: [PATCH 063/179] Add state attributes translation and available modes for Sensibo (#85234) * Sensibo translation climate * Add available states * Fix keys * Delete en.json * invalid fan_mode and swing_mode * Translations * Add back sorting * Fix fan_mode and swing_mode * Fix raise error * review --- homeassistant/components/sensibo/climate.py | 25 ++++++++ homeassistant/components/sensibo/strings.json | 36 ++++++++++- tests/components/sensibo/test_climate.py | 59 +++++++++++++++++-- 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 4ff63a25455..da86ba8fe24 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -54,6 +54,22 @@ ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode" ATTR_LIGHT = "light" BOOST_INCLUSIVE = "boost_inclusive" +AVAILABLE_FAN_MODES = {"quiet", "low", "medium", "medium_high", "high", "auto"} +AVAILABLE_SWING_MODES = { + "stopped", + "fixedtop", + "fixedmiddletop", + "fixedmiddle", + "fixedmiddlebottom", + "fixedbottom", + "rangetop", + "rangemiddle", + "rangebottom", + "rangefull", + "horizontal", + "both", +} + PARALLEL_UPDATES = 0 FIELD_TO_FLAG = { @@ -178,6 +194,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): ) self._attr_supported_features = self.get_features() self._attr_precision = PRECISION_TENTHS + self._attr_translation_key = "climate_device" def get_features(self) -> ClimateEntityFeature: """Get supported features.""" @@ -309,6 +326,10 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Set new target fan mode.""" if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Fanlevel") + if fan_mode not in AVAILABLE_FAN_MODES: + raise HomeAssistantError( + f"Climate fan mode {fan_mode} is not supported by the integration, please open an issue" + ) transformation = self.device_data.fan_modes_translated await self.async_send_api_call( @@ -350,6 +371,10 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Set new target swing operation.""" if "swing" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Swing") + if swing_mode not in AVAILABLE_SWING_MODES: + raise HomeAssistantError( + f"Climate swing mode {swing_mode} is not supported by the integration, please open an issue" + ) transformation = self.device_data.swing_modes_translated await self.async_send_api_call( diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 38ae94d4fa3..a6f14b73ace 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -79,7 +79,9 @@ "fixedright": "Fixed right", "fixedleftright": "Fixed left right", "rangecenter": "Range center", - "rangefull": "Range full" + "rangefull": "Range full", + "rangeleft": "Range left", + "rangeright": "Range right" } }, "light": { @@ -338,6 +340,38 @@ "fw_ver_available": { "name": "Update available" } + }, + "climate": { + "climate_device": { + "state_attributes": { + "fan_mode": { + "state": { + "quiet": "Quiet", + "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", + "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", + "medium_high": "Medium high", + "high": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::high%]", + "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]" + } + }, + "swing_mode": { + "state": { + "stopped": "[%key:common::state::off%]", + "fixedtop": "Fixed top", + "fixedmiddletop": "Fixed middle top", + "fixedmiddle": "Fixed middle", + "fixedmiddlebottom": "Fixed middle bottom", + "fixedbottom": "Fixed bottom", + "rangetop": "Range top", + "rangemiddle": "Range middle", + "rangebottom": "Range bottom", + "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]", + "horizontal": "Horizontal", + "both": "[%key:component::climate::entity_component::_::state_attributes::swing_mode::state::both%]" + } + } + } + } } }, "services": { diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 56a7a8c902c..688a373b8f0 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -101,11 +101,7 @@ async def test_climate( "max_temp": 20, "target_temp_step": 1, "fan_modes": ["low", "medium", "quiet"], - "swing_modes": [ - "fixedmiddletop", - "fixedtop", - "stopped", - ], + "swing_modes": ["fixedmiddletop", "fixedtop", "stopped"], "current_temperature": 21.2, "temperature": 25, "current_humidity": 32.9, @@ -1336,3 +1332,56 @@ async def test_climate_full_ac_state( assert state.state == "cool" assert state.attributes["temperature"] == 22 + + +async def test_climate_fan_mode_and_swing_mode_not_supported( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo climate fan_mode and swing_mode not supported is raising error.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["fan_mode"] == "high" + assert state1.attributes["swing_mode"] == "stopped" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), pytest.raises( + HomeAssistantError, + match="Climate swing mode faulty_swing_mode is not supported by the integration, please open an issue", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "faulty_swing_mode"}, + blocking=True, + ) + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), pytest.raises( + HomeAssistantError, + match="Climate fan mode faulty_fan_mode is not supported by the integration, please open an issue", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "faulty_fan_mode"}, + blocking=True, + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["fan_mode"] == "high" + assert state2.attributes["swing_mode"] == "stopped" From d7a1b1e941449527a461dd5c3d5b0993733bb152 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 8 Aug 2023 19:15:06 +0200 Subject: [PATCH 064/179] Fallback to get_hosts_info on older Fritz!OS in AVM Fritz!Tools (#97844) --- homeassistant/components/fritz/common.py | 119 +++++++++++++++++------ 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 8dfe5be9308..531c05eea4a 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -160,6 +160,15 @@ HostAttributes = TypedDict( ) +class HostInfo(TypedDict): + """FRITZ!Box host info class.""" + + mac: str + name: str + ip: str + status: bool + + class UpdateCoordinatorDataType(TypedDict): """Update coordinator data type.""" @@ -380,16 +389,86 @@ class FritzBoxTools( """Event specific per FRITZ!Box entry to signal updates in devices.""" return f"{DOMAIN}-device-update-{self._unique_id}" - async def _async_update_hosts_info(self) -> list[HostAttributes]: - """Retrieve latest hosts information from the FRITZ!Box.""" + async def _async_get_wan_access(self, ip_address: str) -> bool | None: + """Get WAN access rule for given IP address.""" try: - return await self.hass.async_add_executor_job( - self.fritz_hosts.get_hosts_attributes + wan_access = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=ip_address, + ) ) + return not wan_access.get("NewDisallow") + except FRITZ_EXCEPTIONS as ex: + _LOGGER.debug( + ( + "could not get WAN access rule for client device with IP '%s'," + " error: %s" + ), + ip_address, + ex, + ) + return None + + async def _async_update_hosts_info(self) -> dict[str, Device]: + """Retrieve latest hosts information from the FRITZ!Box.""" + hosts_attributes: list[HostAttributes] = [] + hosts_info: list[HostInfo] = [] + try: + try: + hosts_attributes = await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_attributes + ) + except FritzActionError: + hosts_info = await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_info + ) except Exception as ex: # pylint: disable=[broad-except] if not self.hass.is_stopping: raise HomeAssistantError("Error refreshing hosts info") from ex - return [] + + hosts: dict[str, Device] = {} + if hosts_attributes: + for attributes in hosts_attributes: + if not attributes.get("MACAddress"): + continue + + if (wan_access := attributes.get("X_AVM-DE_WANAccess")) is not None: + wan_access_result = "granted" in wan_access + else: + wan_access_result = None + + hosts[attributes["MACAddress"]] = Device( + name=attributes["HostName"], + connected=attributes["Active"], + connected_to="", + connection_type="", + ip_address=attributes["IPAddress"], + ssid=None, + wan_access=wan_access_result, + ) + else: + for info in hosts_info: + if not info.get("mac"): + continue + + if info["ip"]: + wan_access_result = await self._async_get_wan_access(info["ip"]) + else: + wan_access_result = None + + hosts[info["mac"]] = Device( + name=info["name"], + connected=info["status"], + connected_to="", + connection_type="", + ip_address=info["ip"], + ssid=None, + wan_access=wan_access_result, + ) + return hosts def _update_device_info(self) -> tuple[bool, str | None, str | None]: """Retrieve latest device information from the FRITZ!Box.""" @@ -464,25 +543,7 @@ class FritzBoxTools( consider_home = _default_consider_home new_device = False - hosts = {} - for host in await self._async_update_hosts_info(): - if not host.get("MACAddress"): - continue - - if (wan_access := host.get("X_AVM-DE_WANAccess")) is not None: - wan_access_result = "granted" in wan_access - else: - wan_access_result = None - - hosts[host["MACAddress"]] = Device( - name=host["HostName"], - connected=host["Active"], - connected_to="", - connection_type="", - ip_address=host["IPAddress"], - ssid=None, - wan_access=wan_access_result, - ) + hosts = await self._async_update_hosts_info() if not self.fritz_status.device_has_mesh_support or ( self._options @@ -584,9 +645,7 @@ class FritzBoxTools( self, config_entry: ConfigEntry | None = None ) -> None: """Trigger device trackers cleanup.""" - device_hosts_list = await self.hass.async_add_executor_job( - self.fritz_hosts.get_hosts_attributes - ) + device_hosts = await self._async_update_hosts_info() entity_reg: er.EntityRegistry = er.async_get(self.hass) if config_entry is None: @@ -601,9 +660,9 @@ class FritzBoxTools( device_hosts_macs = set() device_hosts_names = set() - for device in device_hosts_list: - device_hosts_macs.add(device["MACAddress"]) - device_hosts_names.add(device["HostName"]) + for mac, device in device_hosts.items(): + device_hosts_macs.add(mac) + device_hosts_names.add(device.name) for entry in ha_entity_reg_list: if entry.original_name is None: From ba3f0372f363eeb95a7f4eed9f02562addd0d5aa Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 8 Aug 2023 18:15:39 +0100 Subject: [PATCH 065/179] Fix duplicated word in imap_email_content deprecation issue description (#98051) --- homeassistant/components/imap_email_content/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/imap_email_content/strings.json b/homeassistant/components/imap_email_content/strings.json index f84435971bf..b7b987b1212 100644 --- a/homeassistant/components/imap_email_content/strings.json +++ b/homeassistant/components/imap_email_content/strings.json @@ -2,7 +2,7 @@ "issues": { "deprecation": { "title": "The IMAP email content integration is deprecated", - "description": "The IMAP email content integration is deprecated. Your IMAP server configuration was already migrated to to the [imap integration](https://my.home-assistant.io/redirect/config_flow_start?domain=imap). To set up a sensor for the IMAP email content, set up a template sensor with the config:\n\n```yaml\n{yaml_example}```\n\nPlease remove the deprecated `imap_email_plaform` sensor configuration from your `configuration.yaml`.\n\nNote that the event filter only filters on the first of the configured allowed senders, customize the filter if needed.\n\nYou can skip this part if you have already set up a template sensor." + "description": "The IMAP email content integration is deprecated. Your IMAP server configuration was already migrated to the [imap integration](https://my.home-assistant.io/redirect/config_flow_start?domain=imap). To set up a sensor for the IMAP email content, set up a template sensor with the config:\n\n```yaml\n{yaml_example}```\n\nPlease remove the deprecated `imap_email_plaform` sensor configuration from your `configuration.yaml`.\n\nNote that the event filter only filters on the first of the configured allowed senders, customize the filter if needed.\n\nYou can skip this part if you have already set up a template sensor." }, "migration": { "title": "The IMAP email content integration needs attention", From a77009c3ca13f62a59edd1393819c2a0608e2705 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 19:16:52 +0200 Subject: [PATCH 066/179] Patch dt_util.utcnow earlier (#98050) --- tests/conftest.py | 14 +++----------- tests/patch_time.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 tests/patch_time.py diff --git a/tests/conftest.py b/tests/conftest.py index 0b63ddec6af..31900dff6de 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Coroutine, Generator from contextlib import asynccontextmanager -import datetime import functools import gc import itertools @@ -32,6 +31,9 @@ import pytest_socket import requests_mock from syrupy.assertion import SnapshotAssertion +# Setup patching if dt_util time functions before any other Home Assistant imports +from . import patch_time # noqa: F401, isort:skip + from homeassistant import core as ha, loader, runner from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials @@ -53,7 +55,6 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, device_registry as dr, entity_registry as er, - event, issue_registry as ir, recorder as recorder_helper, ) @@ -109,15 +110,6 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) asyncio.set_event_loop_policy = lambda policy: None -def _utcnow() -> datetime.datetime: - """Make utcnow patchable by freezegun.""" - return datetime.datetime.now(datetime.UTC) - - -dt_util.utcnow = _utcnow # type: ignore[assignment] -event.time_tracker_utcnow = _utcnow # type: ignore[assignment] - - def pytest_addoption(parser: pytest.Parser) -> None: """Register custom pytest options.""" parser.addoption("--dburl", action="store", default="sqlite://") diff --git a/tests/patch_time.py b/tests/patch_time.py new file mode 100644 index 00000000000..2a453053170 --- /dev/null +++ b/tests/patch_time.py @@ -0,0 +1,16 @@ +"""Patch time related functions.""" +from __future__ import annotations + +import datetime + +from homeassistant import util +from homeassistant.util import dt as dt_util + + +def _utcnow() -> datetime.datetime: + """Make utcnow patchable by freezegun.""" + return datetime.datetime.now(datetime.UTC) + + +dt_util.utcnow = _utcnow # type: ignore[assignment] +util.utcnow = _utcnow # type: ignore[assignment] From 314d91692f1e4f64baf2580e215a86b37dacf7c8 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 8 Aug 2023 13:39:26 -0400 Subject: [PATCH 067/179] Bump AIOAladdinConnect to 0.1.57 (#98056) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 2702f2e8dec..3f31a833f1a 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], - "requirements": ["AIOAladdinConnect==0.1.56"] + "requirements": ["AIOAladdinConnect==0.1.57"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7cc1ca2cdc3..351430150ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.2 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.56 +AIOAladdinConnect==0.1.57 # homeassistant.components.honeywell AIOSomecomfort==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e7bc5dc41f..7f31f63a623 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.2 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.56 +AIOAladdinConnect==0.1.57 # homeassistant.components.honeywell AIOSomecomfort==0.0.15 From 8f2e30040ca8b58cfa2bf51c52e864c02b3d5811 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 8 Aug 2023 19:39:41 +0200 Subject: [PATCH 068/179] Add DeviceInfo to Scrape (#97399) * Add DeviceInfo to Scrape * simplify * review comment --- homeassistant/components/scrape/sensor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index cc4cd269606..f2c186be9e6 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -24,6 +24,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ( @@ -164,6 +166,15 @@ class ScrapeSensor( self._index = index self._value_template = value_template self._attr_native_value = None + if not yaml and (unique_id := trigger_entity_config.get(CONF_UNIQUE_ID)): + self._attr_name = None + self._attr_has_entity_name = True + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer="Scrape", + name=self.name, + ) def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" From 45d4c307de86d65c9db20749a8028efdceed845c Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 8 Aug 2023 18:43:09 +0100 Subject: [PATCH 069/179] Hyphenate "human-readable" in light service description (#98057) --- homeassistant/components/light/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 5398d38ca5d..80e2ca54562 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -264,7 +264,7 @@ }, "color_name": { "name": "Color name", - "description": "A human readable color name." + "description": "A human-readable color name." }, "hs_color": { "name": "Hue/Sat color", From c15407717753d66cd07082aad90669ea0586be09 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 8 Aug 2023 14:03:02 -0400 Subject: [PATCH 070/179] Add Encharge binary sensors to Enphase integration (#98039) * Add Encharge binary sensors to Enphase integration * Code review minor cleanup * Add to coveragerc --- .coveragerc | 1 + .../components/enphase_envoy/binary_sensor.py | 141 ++++++++++++++++++ .../components/enphase_envoy/const.py | 2 +- .../components/enphase_envoy/strings.json | 11 ++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/enphase_envoy/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2e35001ee14..cb9ca19c5b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -302,6 +302,7 @@ omit = homeassistant/components/enocean/sensor.py homeassistant/components/enocean/switch.py homeassistant/components/enphase_envoy/__init__.py + homeassistant/components/enphase_envoy/binary_sensor.py homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py new file mode 100644 index 00000000000..af57a4da6af --- /dev/null +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -0,0 +1,141 @@ +"""Support for Enphase Envoy solar energy monitor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from pyenphase import ( + EnvoyData, + EnvoyEncharge, +) + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +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 DOMAIN +from .coordinator import EnphaseUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class EnvoyEnchargeRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEncharge], bool] + + +@dataclass +class EnvoyEnchargeBinarySensorEntityDescription( + BinarySensorEntityDescription, EnvoyEnchargeRequiredKeysMixin +): + """Describes an Envoy Encharge binary sensor entity.""" + + +ENCHARGE_SENSORS = ( + EnvoyEnchargeBinarySensorEntityDescription( + key="communicating", + translation_key="communicating", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda encharge: encharge.communicating, + ), + EnvoyEnchargeBinarySensorEntityDescription( + key="dc_switch", + translation_key="dc_switch", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda encharge: not encharge.dc_switch_off, + ), + EnvoyEnchargeBinarySensorEntityDescription( + key="operating", + translation_key="operating", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda encharge: encharge.operating, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up envoy binary sensor platform.""" + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + envoy_data = coordinator.envoy.data + assert envoy_data is not None + envoy_serial_num = config_entry.unique_id + assert envoy_serial_num is not None + entities: list[BinarySensorEntity] = [] + if envoy_data.encharge_inventory: + entities.extend( + EnvoyEnchargeBinarySensorEntity(coordinator, description, encharge) + for description in ENCHARGE_SENSORS + for encharge in envoy_data.encharge_inventory + ) + + async_add_entities(entities) + + +class EnvoyEnchargeBinarySensorEntity( + CoordinatorEntity[EnphaseUpdateCoordinator], BinarySensorEntity +): + """Defines a base envoy binary_sensor entity.""" + + _attr_has_entity_name = True + entity_description: EnvoyEnchargeBinarySensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnchargeBinarySensorEntityDescription, + serial_number: str, + ) -> None: + """Init the Encharge base entity.""" + self.entity_description = description + self.coordinator = coordinator + assert serial_number is not None + + self.envoy_serial_num = coordinator.envoy.serial_number + assert self.envoy_serial_num is not None + + self._serial_number = serial_number + self._attr_unique_id = f"{serial_number}_{description.key}" + encharge_inventory = self.data.encharge_inventory + assert encharge_inventory is not None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_number)}, + manufacturer="Enphase", + model="Encharge", + name=f"Encharge {serial_number}", + sw_version=str(encharge_inventory[self._serial_number].firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + + super().__init__(coordinator) + + @property + def data(self) -> EnvoyData: + """Return envoy data.""" + data = self.coordinator.envoy.data + assert data is not None + return data + + @property + def is_on(self) -> bool: + """Return the state of the Encharge binary_sensor.""" + encharge_inventory = self.data.encharge_inventory + assert encharge_inventory is not None + return self.entity_description.value_fn(encharge_inventory[self._serial_number]) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 029453660fd..662662aa8be 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -8,6 +8,6 @@ from homeassistant.const import Platform DOMAIN = "enphase_envoy" -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index d503dacb2d8..46ec7d9607f 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -22,6 +22,17 @@ } }, "entity": { + "binary_sensor": { + "communicating": { + "name": "Communicating" + }, + "dc_switch": { + "name": "DC Switch" + }, + "operating": { + "name": "Operating" + } + }, "sensor": { "last_reported": { "name": "Last reported" From 3624e30380f4664f9a8422def1536ed9fb723230 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 8 Aug 2023 19:13:19 +0100 Subject: [PATCH 071/179] Update silabs_multiprotocol_hardware change cannel options flow description (#98047) strings.json: Update silabs_multiprotocol_hardware::options message * Removes trailing space * Fixes double space * Adds word before noun --- homeassistant/components/homeassistant_hardware/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 45e85f5a474..24cd049668d 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -23,7 +23,7 @@ "data": { "channel": "Channel" }, - "description": "Start a channel change for your Zigbee and Thread networks.\n\nNote: this is an advanced operation and can leave your Thread and Zigbee networks inoperable if the new channel is congested. Depending on existing network conditions, many of your devices may not migrate to the new channel and will require re-joining before they start working again. Use with caution.\n\nOnce you selected **Submit**, the channel change starts quietly in the background and will finish after a few minutes. " + "description": "Start a channel change for your Zigbee and Thread networks.\n\nNote: this is an advanced operation and can leave your Thread and Zigbee networks inoperable if the new channel is congested. Depending on existing network conditions, many of your devices may not migrate to the new channel and will require re-joining before they start working again. Use with caution.\n\nOnce you have selected **Submit**, the channel change starts quietly in the background and will finish after a few minutes." }, "install_addon": { "title": "The Silicon Labs Multiprotocol add-on installation has started" From 66e3d6960612fba860e4b455192da492845acd38 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Aug 2023 22:02:45 +0200 Subject: [PATCH 072/179] Remove confusing comment from accuweather (#98063) --- homeassistant/components/accuweather/weather.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 30dae28c408..c2889bae102 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -68,9 +68,6 @@ class AccuWeatherEntity( def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) - # Coordinator data is used also for sensors which don't have units automatically - # converted, hence the weather entity's native units follow the configured unit - # system self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS From 524a26d9e1ee69b909623ddc76c09c87d735ca94 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 8 Aug 2023 22:05:12 +0200 Subject: [PATCH 073/179] Add entity translations to Neato (#98067) * Add entity translations to Neato * Use robot name --- homeassistant/components/neato/button.py | 8 ++++++-- homeassistant/components/neato/camera.py | 14 +++++++------- homeassistant/components/neato/sensor.py | 13 ++++++------- homeassistant/components/neato/strings.json | 17 +++++++++++++++++ homeassistant/components/neato/switch.py | 14 +++++++------- homeassistant/components/neato/vacuum.py | 5 +++-- 6 files changed, 46 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/neato/button.py b/homeassistant/components/neato/button.py index f215bbe7225..ba0438998f6 100644 --- a/homeassistant/components/neato/button.py +++ b/homeassistant/components/neato/button.py @@ -25,6 +25,8 @@ async def async_setup_entry( class NeatoDismissAlertButton(ButtonEntity): """Representation of a dismiss_alert button entity.""" + _attr_has_entity_name = True + _attr_translation_key = "dismiss_alert" _attr_entity_category = EntityCategory.CONFIG def __init__( @@ -33,9 +35,11 @@ class NeatoDismissAlertButton(ButtonEntity): ) -> None: """Initialize a dismiss_alert Neato button entity.""" self.robot = robot - self._attr_name = f"{robot.name} Dismiss Alert" self._attr_unique_id = f"{robot.serial}_dismiss_alert" - self._attr_device_info = DeviceInfo(identifiers={(NEATO_DOMAIN, robot.serial)}) + self._attr_device_info = DeviceInfo( + identifiers={(NEATO_DOMAIN, robot.serial)}, + name=robot.name, + ) async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 6429056afa1..da50e528d3c 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -51,6 +51,9 @@ async def async_setup_entry( class NeatoCleaningMap(Camera): """Neato cleaning map for last clean.""" + _attr_has_entity_name = True + _attr_translation_key = "cleaning_map" + def __init__( self, neato: NeatoHub, robot: Robot, mapdata: dict[str, Any] | None ) -> None: @@ -60,7 +63,6 @@ class NeatoCleaningMap(Camera): self.neato = neato self._mapdata = mapdata self._available = neato is not None - self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial: str = self.robot.serial self._generated_at: str | None = None self._image_url: str | None = None @@ -114,11 +116,6 @@ class NeatoCleaningMap(Camera): self._generated_at = map_data.get("generated_at") self._available = True - @property - def name(self) -> str: - """Return the name of this camera.""" - return self._robot_name - @property def unique_id(self) -> str: """Return unique ID.""" @@ -132,7 +129,10 @@ class NeatoCleaningMap(Camera): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - return DeviceInfo(identifiers={(NEATO_DOMAIN, self._robot_serial)}) + return DeviceInfo( + identifiers={(NEATO_DOMAIN, self._robot_serial)}, + name=self.robot.name, + ) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 3831c68ac6c..2b8e0b3bf8b 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -44,11 +44,12 @@ async def async_setup_entry( class NeatoSensor(SensorEntity): """Neato sensor.""" + _attr_has_entity_name = True + def __init__(self, neato: NeatoHub, robot: Robot) -> None: """Initialize Neato sensor.""" self.robot = robot self._available: bool = False - self._robot_name: str = f"{self.robot.name} {BATTERY}" self._robot_serial: str = self.robot.serial self._state: dict[str, Any] | None = None @@ -68,11 +69,6 @@ class NeatoSensor(SensorEntity): self._available = True _LOGGER.debug("self._state=%s", self._state) - @property - def name(self) -> str: - """Return the name of this sensor.""" - return self._robot_name - @property def unique_id(self) -> str: """Return unique ID.""" @@ -108,4 +104,7 @@ class NeatoSensor(SensorEntity): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - return DeviceInfo(identifiers={(NEATO_DOMAIN, self._robot_serial)}) + return DeviceInfo( + identifiers={(NEATO_DOMAIN, self._robot_serial)}, + name=self.robot.name, + ) diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index 6136ac94e99..d611abb83b0 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -19,6 +19,23 @@ "default": "[%key:common::config_flow::create_entry::authenticated%]" } }, + "entity": { + "button": { + "dismiss_alert": { + "name": "Dismiss alert" + } + }, + "camera": { + "cleaning_map": { + "name": "Cleaning map" + } + }, + "switch": { + "schedule": { + "name": "Schedule" + } + } + }, "services": { "custom_cleaning": { "name": "Zone cleaning service", diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 0619e616b98..6fba5327290 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -48,12 +48,14 @@ async def async_setup_entry( class NeatoConnectedSwitch(SwitchEntity): """Neato Connected Switches.""" + _attr_has_entity_name = True + _attr_translation_key = "schedule" + def __init__(self, neato: NeatoHub, robot: Robot, switch_type: str) -> None: """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot self._available = False - self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state: dict[str, Any] | None = None self._schedule_state: str | None = None self._clean_state = None @@ -85,11 +87,6 @@ class NeatoConnectedSwitch(SwitchEntity): "Schedule state for '%s': %s", self.entity_id, self._schedule_state ) - @property - def name(self) -> str: - """Return the name of the switch.""" - return self._robot_name - @property def available(self) -> bool: """Return True if entity is available.""" @@ -115,7 +112,10 @@ class NeatoConnectedSwitch(SwitchEntity): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - return DeviceInfo(identifiers={(NEATO_DOMAIN, self._robot_serial)}) + return DeviceInfo( + identifiers={(NEATO_DOMAIN, self._robot_serial)}, + name=self.robot.name, + ) def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 4d402fbb8bb..b10b1f83eac 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -106,6 +106,8 @@ class NeatoConnectedVacuum(StateVacuumEntity): | VacuumEntityFeature.MAP | VacuumEntityFeature.LOCATE ) + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -118,7 +120,6 @@ class NeatoConnectedVacuum(StateVacuumEntity): self.robot = robot self._attr_available: bool = neato is not None self._mapdata = mapdata - self._attr_name: str = self.robot.name self._robot_has_map: bool = self.robot.has_persistent_maps self._robot_maps = persistent_maps self._robot_serial: str = self.robot.serial @@ -304,7 +305,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): identifiers={(NEATO_DOMAIN, self._robot_serial)}, manufacturer=stats["battery"]["vendor"] if stats else None, model=stats["model"] if stats else None, - name=self._attr_name, + name=self.robot.name, sw_version=stats["firmware"] if stats else None, ) From 25467b573e565b854dfc2c1182d65c77536d1d39 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 8 Aug 2023 17:27:59 -0400 Subject: [PATCH 074/179] Bump pyenphase to 1.1.1 (#98065) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index c21a0138d21..901473b751e 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==0.15.1"], + "requirements": ["pyenphase==1.1.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 351430150ae..ecea928b163 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==0.15.1 +pyenphase==1.1.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f31f63a623..0c9ba4ae69e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==0.15.1 +pyenphase==1.1.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 5c9bce9eac62c2e2509ae98139a2dbd25d256b5f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Aug 2023 23:44:49 +0200 Subject: [PATCH 075/179] Allow float for inital MQTT climate temperature (#97995) * Allow float for inital MQTT climate temperature * Update tests/components/mqtt/test_climate.py Co-authored-by: Erik Montnemery --------- Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/climate.py | 2 +- tests/components/mqtt/test_climate.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f45d2852df0..b95cacc2d08 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -327,7 +327,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): cv.ensure_list, vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_TEMP_INITIAL): cv.positive_int, + vol.Optional(CONF_TEMP_INITIAL): vol.All(vol.Coerce(float)), vol.Optional(CONF_TEMP_MIN): vol.Coerce(float), vol.Optional(CONF_TEMP_MAX): vol.Coerce(float), vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index e717c04b317..18a0a860ad4 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1868,6 +1868,24 @@ async def test_temperature_unit( DEFAULT_MAX_TEMP, 25, ), + ( + help_custom_config( + climate.DOMAIN, + DEFAULT_CONFIG, + ( + { + "initial": 68.9, # 20.5 °C + "temperature_unit": "F", + "current_temperature_topic": "current_temperature", + }, + ), + ), + UnitOfTemperature.CELSIUS, + 20.5, + DEFAULT_MIN_TEMP, + DEFAULT_MAX_TEMP, + 25, + ), ( help_custom_config( climate.DOMAIN, From 331bdcc5969eb424d9653382bbdf40afe65fb638 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Aug 2023 12:51:17 -1000 Subject: [PATCH 076/179] Bump pyenphase to 1.1.3 (#98074) changelog: https://github.com/pyenphase/pyenphase/compare/v1.1.1...v1.1.3 --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 901473b751e..9656dbe9084 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.1.1"], + "requirements": ["pyenphase==1.1.3"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index ecea928b163..0496849e477 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.1.1 +pyenphase==1.1.3 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c9ba4ae69e..15ef1794dfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.1.1 +pyenphase==1.1.3 # homeassistant.components.everlights pyeverlights==0.1.0 From d975e93abc2afcb3e1060799f1b84ce74fe56cc4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 9 Aug 2023 01:16:55 +0200 Subject: [PATCH 077/179] Add entity translations for Ambient station (#98075) * Add entity translations for Ambient station * Fix missed key --- .../ambient_station/binary_sensor.py | 87 +++-- .../components/ambient_station/sensor.py | 152 ++++---- .../components/ambient_station/strings.json | 351 ++++++++++++++++++ 3 files changed, 466 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index ca32f16c758..a58a0ec6f85 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -80,304 +80,303 @@ class AmbientBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( AmbientBinarySensorDescription( key=TYPE_BATTOUT, - name="Battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT1, - name="Battery 1", + translation_key="battery_1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT2, - name="Battery 2", + translation_key="battery_2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT3, - name="Battery 3", + translation_key="battery_3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT4, - name="Battery 4", + translation_key="battery_4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT5, - name="Battery 5", + translation_key="battery_5", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT6, - name="Battery 6", + translation_key="battery_6", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT7, - name="Battery 7", + translation_key="battery_7", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT8, - name="Battery 8", + translation_key="battery_8", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT9, - name="Battery 9", + translation_key="battery_9", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATTIN, - name="Interior battery", + translation_key="interior_battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, - name="Battery 10", + translation_key="battery_10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK1, - name="Leak detector battery 1", + translation_key="leak_detector_battery_1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK2, - name="Leak detector battery 2", + translation_key="leak_detector_battery_2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK3, - name="Leak detector battery 3", + translation_key="leak_detector_battery_3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK4, - name="Leak detector battery 4", + translation_key="leak_detector_battery_4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM1, - name="Soil monitor battery 1", + translation_key="soil_monitor_battery_1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM2, - name="Soil monitor battery 2", + translation_key="soil_monitor_battery_2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM3, - name="Soil monitor battery 3", + translation_key="soil_monitor_battery_3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM4, - name="Soil monitor battery 4", + translation_key="soil_monitor_battery_4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM5, - name="Soil monitor battery 5", + translation_key="soil_monitor_battery_5", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM6, - name="Soil monitor battery 6", + translation_key="soil_monitor_battery_6", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM7, - name="Soil monitor battery 7", + translation_key="soil_monitor_battery_7", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM8, - name="Soil monitor battery 8", + translation_key="soil_monitor_battery_8", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM9, - name="Soil monitor battery 9", + translation_key="soil_monitor_battery_9", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM10, - name="Soil monitor battery 10", + translation_key="soil_monitor_battery_10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, - name="CO2 battery", + translation_key="co2_battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_LIGHTNING, - name="Lightning detector battery", + translation_key="lightning_detector_battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_LEAK1, - name="Leak detector 1", + translation_key="leak_detector_1", device_class=BinarySensorDeviceClass.MOISTURE, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_LEAK2, - name="Leak detector 2", + translation_key="leak_detector_2", device_class=BinarySensorDeviceClass.MOISTURE, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_LEAK3, - name="Leak detector 3", + translation_key="leak_detector_3", device_class=BinarySensorDeviceClass.MOISTURE, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_LEAK4, - name="Leak detector 4", + translation_key="leak_detector_4", device_class=BinarySensorDeviceClass.MOISTURE, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, - name="PM25 indoor battery", + translation_key="pm25_indoor_battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, - name="PM25 battery", + translation_key="pm25_battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_RELAY1, - name="Relay 1", + translation_key="relay_1", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY2, - name="Relay 2", + translation_key="relay_2", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY3, - name="Relay 3", + translation_key="relay_3", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY4, - name="Relay 4", + translation_key="relay_4", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY5, - name="Relay 5", + translation_key="relay_5", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY6, - name="Relay 6", + translation_key="relay_6", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY7, - name="Relay 7", + translation_key="relay_7", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY8, - name="Relay 8", + translation_key="relay_8", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY9, - name="Relay 9", + translation_key="relay_9", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY10, - name="Relay 10", + translation_key="relay_10", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, on_state=1, diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 8bdc66133d6..e1f624da52f 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -113,544 +113,536 @@ TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_24HOURRAININ, - name="24 hr rain", + translation_key="24_hour_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25, - name="AQI PM2.5", + translation_key="pm25_aqi", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_AQI_PM25_24H, - name="AQI PM2.5 24h avg", + translation_key="pm25_aqi_24h_average", device_class=SensorDeviceClass.AQI, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, - name="AQI PM2.5 indoor", + translation_key="pm25_indoor_aqi", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN_24H, - name="AQI PM2.5 indoor 24h avg", + translation_key="pm25_indoor_aqi_24h_average", device_class=SensorDeviceClass.AQI, ), SensorEntityDescription( key=TYPE_BAROMABSIN, - name="Abs pressure", + translation_key="absolute_pressure", native_unit_of_measurement=UnitOfPressure.INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_BAROMRELIN, - name="Rel pressure", + translation_key="relative_pressure", native_unit_of_measurement=UnitOfPressure.INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CO2, - name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_DAILYRAININ, - name="Daily rain", + translation_key="daily_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_DEWPOINT, - name="Dew point", + translation_key="dew_point", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_EVENTRAININ, - name="Event rain", + translation_key="event_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_FEELSLIKE, - name="Feels like", + translation_key="feels_like", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HOURLYRAININ, - name="Hourly rain rate", native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, ), SensorEntityDescription( key=TYPE_HUMIDITY10, - name="Humidity 10", + translation_key="humidity_10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY1, - name="Humidity 1", + translation_key="humidity_1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY2, - name="Humidity 2", + translation_key="humidity_2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY3, - name="Humidity 3", + translation_key="humidity_3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY4, - name="Humidity 4", + translation_key="humidity_4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY5, - name="Humidity 5", + translation_key="humidity_5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY6, - name="Humidity 6", + translation_key="humidity_6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY7, - name="Humidity 7", + translation_key="humidity_7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY8, - name="Humidity 8", + translation_key="humidity_8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY9, - name="Humidity 9", + translation_key="humidity_9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY, - name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITYIN, - name="Humidity in", + translation_key="humidity_indoor", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LASTRAIN, - name="Last rain", + translation_key="last_rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, - name="Lightning strikes per day", + translation_key="lightning_strikes_per_day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, - name="Lightning strikes per hour", + translation_key="lightning_strikes_per_hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, - name="Max gust", + translation_key="max_gust", native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_MONTHLYRAININ, - name="Monthly rain", + translation_key="monthly_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_PM25_24H, - name="PM25 24h avg", + translation_key="pm25_24h_average", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25_IN, - name="PM25 indoor", + translation_key="pm25_indoor", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_IN_24H, - name="PM25 indoor 24h avg", + translation_key="pm25_indoor_24h_average", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25, - name="PM25", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM10, - name="Soil humidity 10", + translation_key="soil_humidity_10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM1, - name="Soil humidity 1", + translation_key="soil_humidity_1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM2, - name="Soil humidity 2", + translation_key="soil_humidity_2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM3, - name="Soil humidity 3", + translation_key="soil_humidity_3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM4, - name="Soil humidity 4", + translation_key="soil_humidity_4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM5, - name="Soil humidity 5", + translation_key="soil_humidity_5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM6, - name="Soil humidity 6", + translation_key="soil_humidity_6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM7, - name="Soil humidity 7", + translation_key="soil_humidity_7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM8, - name="Soil humidity 8", + translation_key="soil_humidity_8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM9, - name="Soil humidity 9", + translation_key="soil_humidity_9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, - name="Soil temp 10", + translation_key="soil_temperature_10", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP1F, - name="Soil temp 1", + translation_key="soil_temperature_1", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP2F, - name="Soil temp 2", + translation_key="soil_temperature_2", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP3F, - name="Soil temp 3", + translation_key="soil_temperature_3", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP4F, - name="Soil temp 4", + translation_key="soil_temperature_4", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP5F, - name="Soil temp 5", + translation_key="soil_temperature_5", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP6F, - name="Soil temp 6", + translation_key="soil_temperature_6", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP7F, - name="Soil temp 7", + translation_key="soil_temperature_7", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP8F, - name="Soil temp 8", + translation_key="soil_temperature_8", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP9F, - name="Soil temp 9", + translation_key="soil_temperature_9", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION, - name="Solar rad", native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, device_class=SensorDeviceClass.IRRADIANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION_LX, - name="Solar rad", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP10F, - name="Temp 10", + translation_key="temperature_10", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP1F, - name="Temp 1", + translation_key="temperature_1", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP2F, - name="Temp 2", + translation_key="temperature_2", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP3F, - name="Temp 3", + translation_key="temperature_3", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP4F, - name="Temp 4", + translation_key="temperature_4", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP5F, - name="Temp 5", + translation_key="temperature_5", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP6F, - name="Temp 6", + translation_key="temperature_6", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP7F, - name="Temp 7", + translation_key="temperature_7", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP8F, - name="Temp 8", + translation_key="temperature_8", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP9F, - name="Temp 9", + translation_key="temperature_9", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMPF, - name="Temp", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMPINF, - name="Inside temp", + translation_key="inside_temperature", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TOTALRAININ, - name="Lifetime rain", + translation_key="lifetime_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_UV, - name="UV index", + translation_key="uv_index", native_unit_of_measurement="Index", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WEEKLYRAININ, - name="Weekly rain", + translation_key="weekly_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_WINDDIR, - name="Wind dir", + translation_key="wind_direction", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG10M, - name="Wind dir avg 10m", + translation_key="wind_direction_average_10m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG2M, - name="Wind dir avg 2m", + translation_key="wind_direction_average_2m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTDIR, - name="Gust dir", + translation_key="wind_gust_direction", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTMPH, - name="Wind gust", + translation_key="wind_gust", native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG10M, - name="Wind avg 10m", + translation_key="wind_average_10m", native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, - name="Wind avg 2m", + translation_key="wind_average_2m", native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, - name="Wind speed", native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_YEARLYRAININ, - name="Yearly rain", + translation_key="yearly_rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL_INCREASING, diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json index a9bce82e10b..02bceda500f 100644 --- a/homeassistant/components/ambient_station/strings.json +++ b/homeassistant/components/ambient_station/strings.json @@ -16,5 +16,356 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "entity": { + "binary_sensor": { + "battery_1": { + "name": "Battery 1" + }, + "battery_2": { + "name": "Battery 2" + }, + "battery_3": { + "name": "Battery 3" + }, + "battery_4": { + "name": "Battery 4" + }, + "battery_5": { + "name": "Battery 5" + }, + "battery_6": { + "name": "Battery 6" + }, + "battery_7": { + "name": "Battery 7" + }, + "battery_8": { + "name": "Battery 8" + }, + "battery_9": { + "name": "Battery 9" + }, + "battery_10": { + "name": "Battery 10" + }, + "interior_battery": { + "name": "Interior battery" + }, + "leak_detector_battery_1": { + "name": "Leak detector battery 1" + }, + "leak_detector_battery_2": { + "name": "Leak detector battery 2" + }, + "leak_detector_battery_3": { + "name": "Leak detector battery 3" + }, + "leak_detector_battery_4": { + "name": "Leak detector battery 4" + }, + "soil_monitor_battery_1": { + "name": "Soil monitor battery 1" + }, + "soil_monitor_battery_2": { + "name": "Soil monitor battery 2" + }, + "soil_monitor_battery_3": { + "name": "Soil monitor battery 3" + }, + "soil_monitor_battery_4": { + "name": "Soil monitor battery 4" + }, + "soil_monitor_battery_5": { + "name": "Soil monitor battery 5" + }, + "soil_monitor_battery_6": { + "name": "Soil monitor battery 6" + }, + "soil_monitor_battery_7": { + "name": "Soil monitor battery 7" + }, + "soil_monitor_battery_8": { + "name": "Soil monitor battery 8" + }, + "soil_monitor_battery_9": { + "name": "Soil monitor battery 9" + }, + "soil_monitor_battery_10": { + "name": "Soil monitor battery 10" + }, + "co2_battery": { + "name": "Carbon dioxide battery" + }, + "lightning_detector_battery": { + "name": "Lightning detector battery" + }, + "leak_detector_1": { + "name": "Leak detector 1" + }, + "leak_detector_2": { + "name": "Leak detector 2" + }, + "leak_detector_3": { + "name": "Leak detector 3" + }, + "leak_detector_4": { + "name": "Leak detector 4" + }, + "pm25_indoor_battery": { + "name": "PM25 indoor battery" + }, + "pm25_battery": { + "name": "PM25 battery" + }, + "relay_1": { + "name": "Relay 1" + }, + "relay_2": { + "name": "Relay 2" + }, + "relay_3": { + "name": "Relay 3" + }, + "relay_4": { + "name": "Relay 4" + }, + "relay_5": { + "name": "Relay 5" + }, + "relay_6": { + "name": "Relay 6" + }, + "relay_7": { + "name": "Relay 7" + }, + "relay_8": { + "name": "Relay 8" + }, + "relay_9": { + "name": "Relay 9" + }, + "relay_10": { + "name": "Relay 10" + } + }, + "sensor": { + "24_hour_rain": { + "name": "Rain 24 hours" + }, + "pm25_aqi": { + "name": "PM2.5 AQI" + }, + "pm25_aqi_24h_average": { + "name": "PM2.5 AQI 24 hour average" + }, + "pm25_indoor_aqi": { + "name": "PM2.5 indoor AQI" + }, + "pm25_indoor_aqi_24h_average": { + "name": "PM2.5 indoor AQI" + }, + "absolute_pressure": { + "name": "Absolute pressure" + }, + "relative_pressure": { + "name": "Relative pressure" + }, + "daily_rain": { + "name": "Daily rain" + }, + "dew_point": { + "name": "Dew point" + }, + "event_rain": { + "name": "Event rain" + }, + "feels_like": { + "name": "Feels like" + }, + "humidity_1": { + "name": "Humidity 1" + }, + "humidity_2": { + "name": "Humidity 2" + }, + "humidity_3": { + "name": "Humidity 3" + }, + "humidity_4": { + "name": "Humidity 4" + }, + "humidity_5": { + "name": "Humidity 5" + }, + "humidity_6": { + "name": "Humidity 6" + }, + "humidity_7": { + "name": "Humidity 7" + }, + "humidity_8": { + "name": "Humidity 8" + }, + "humidity_9": { + "name": "Humidity 9" + }, + "humidity_10": { + "name": "Humidity 10" + }, + "humidity_indoor": { + "name": "Humidity indoor" + }, + "last_rain": { + "name": "Last rain" + }, + "lightning_strikes_per_day": { + "name": "Lightning strikes per day" + }, + "lightning_strikes_per_hour": { + "name": "Lightning strikes per hour" + }, + "max_gust": { + "name": "Max gust" + }, + "monthly_rain": { + "name": "Monthly rain" + }, + "pm25_24h_average": { + "name": "PM2.5 24 hour average" + }, + "pm25_indoor": { + "name": "PM2.5 indoor" + }, + "pm25_indoor_24h_average": { + "name": "PM2.5 indoor 24 hour average" + }, + "soil_humidity_1": { + "name": "Soil humidity 1" + }, + "soil_humidity_2": { + "name": "Soil humidity 2" + }, + "soil_humidity_3": { + "name": "Soil humidity 3" + }, + "soil_humidity_4": { + "name": "Soil humidity 4" + }, + "soil_humidity_5": { + "name": "Soil humidity 5" + }, + "soil_humidity_6": { + "name": "Soil humidity 6" + }, + "soil_humidity_7": { + "name": "Soil humidity 7" + }, + "soil_humidity_8": { + "name": "Soil humidity 8" + }, + "soil_humidity_9": { + "name": "Soil humidity 9" + }, + "soil_humidity_10": { + "name": "Soil humidity 10" + }, + "soil_temperature_1": { + "name": "Soil temperature 1" + }, + "soil_temperature_2": { + "name": "Soil temperature 2" + }, + "soil_temperature_3": { + "name": "Soil temperature 3" + }, + "soil_temperature_4": { + "name": "Soil temperature 4" + }, + "soil_temperature_5": { + "name": "Soil temperature 5" + }, + "soil_temperature_6": { + "name": "Soil temperature 6" + }, + "soil_temperature_7": { + "name": "Soil temperature 7" + }, + "soil_temperature_8": { + "name": "Soil temperature 8" + }, + "soil_temperature_9": { + "name": "Soil temperature 9" + }, + "soil_temperature_10": { + "name": "Soil temperature 10" + }, + "temperature_1": { + "name": "Temperature 1" + }, + "temperature_2": { + "name": "Temperature 2" + }, + "temperature_3": { + "name": "Temperature 3" + }, + "temperature_4": { + "name": "Temperature 4" + }, + "temperature_5": { + "name": "Temperature 5" + }, + "temperature_6": { + "name": "Temperature 6" + }, + "temperature_7": { + "name": "Temperature 7" + }, + "temperature_8": { + "name": "Temperature 8" + }, + "temperature_9": { + "name": "Temperature 9" + }, + "temperature_10": { + "name": "Temperature 10" + }, + "inside_temperature": { + "name": "Inside temperature" + }, + "lifetime_rain": { + "name": "Lifetime rain" + }, + "uv_index": { + "name": "UV index" + }, + "weekly_rain": { + "name": "Weekly rain" + }, + "wind_direction": { + "name": "Wind direction" + }, + "wind_direction_average_10m": { + "name": "Wind direction average 10 minutes" + }, + "wind_direction_average_2m": { + "name": "Wind direction average 2 minutes" + }, + "wind_gust_direction": { + "name": "Wind gust direction" + }, + "wind_gust": { + "name": "Wind gust" + }, + "wind_average_10m": { + "name": "Wind average 10 minutes" + }, + "wind_average_2m": { + "name": "Wind average 2 minutes" + }, + "yearly_rain": { + "name": "Yearly rain" + } + } } } From ce6b759b70de4d029b4151f7ee30b8a6fb5a35f0 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 8 Aug 2023 23:05:52 -0400 Subject: [PATCH 078/179] Add Envoy enpower sensors (#98086) --- .../components/enphase_envoy/binary_sensor.py | 164 ++++++++++++++++-- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 71 ++++++++ .../components/enphase_envoy/strings.json | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 225 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index af57a4da6af..4e893050b16 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -8,7 +8,9 @@ import logging from pyenphase import ( EnvoyData, EnvoyEncharge, + EnvoyEnpower, ) +from pyenphase.models.dry_contacts import DryContactStatus from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -66,6 +68,47 @@ ENCHARGE_SENSORS = ( ), ) +RELAY_STATUS_SENSOR = BinarySensorEntityDescription( + key="relay_status", icon="mdi:power-plug", has_entity_name=True +) + + +@dataclass +class EnvoyEnpowerRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnpower], bool] + + +@dataclass +class EnvoyEnpowerBinarySensorEntityDescription( + BinarySensorEntityDescription, EnvoyEnpowerRequiredKeysMixin +): + """Describes an Envoy Enpower binary sensor entity.""" + + +ENPOWER_SENSORS = ( + EnvoyEnpowerBinarySensorEntityDescription( + key="communicating", + translation_key="communicating", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda enpower: enpower.communicating, + ), + EnvoyEnpowerBinarySensorEntityDescription( + key="operating", + translation_key="operating", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda enpower: enpower.operating, + ), + EnvoyEnpowerBinarySensorEntityDescription( + key="mains_oper_state", + translation_key="grid_status", + icon="mdi:transmission-tower", + value_fn=lambda enpower: enpower.mains_oper_state == "closed", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -86,15 +129,50 @@ async def async_setup_entry( for encharge in envoy_data.encharge_inventory ) + if envoy_data.enpower: + entities.extend( + EnvoyEnpowerBinarySensorEntity(coordinator, description) + for description in ENPOWER_SENSORS + ) + + if envoy_data.dry_contact_status: + entities.extend( + EnvoyRelayBinarySensorEntity(coordinator, RELAY_STATUS_SENSOR, relay) + for relay in envoy_data.dry_contact_status + ) async_add_entities(entities) -class EnvoyEnchargeBinarySensorEntity( +class EnvoyBaseBinarySensorEntity( CoordinatorEntity[EnphaseUpdateCoordinator], BinarySensorEntity ): """Defines a base envoy binary_sensor entity.""" _attr_has_entity_name = True + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: BinarySensorEntityDescription, + ) -> None: + """Init the Enphase base binary_sensor entity.""" + self.entity_description = description + serial_number = coordinator.envoy.serial_number + assert serial_number is not None + self.envoy_serial_num = serial_number + super().__init__(coordinator) + + @property + def data(self) -> EnvoyData: + """Return envoy data.""" + data = self.coordinator.envoy.data + assert data is not None + return data + + +class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an Encharge binary_sensor entity.""" + entity_description: EnvoyEnchargeBinarySensorEntityDescription def __init__( @@ -104,13 +182,7 @@ class EnvoyEnchargeBinarySensorEntity( serial_number: str, ) -> None: """Init the Encharge base entity.""" - self.entity_description = description - self.coordinator = coordinator - assert serial_number is not None - - self.envoy_serial_num = coordinator.envoy.serial_number - assert self.envoy_serial_num is not None - + super().__init__(coordinator, description) self._serial_number = serial_number self._attr_unique_id = f"{serial_number}_{description.key}" encharge_inventory = self.data.encharge_inventory @@ -124,18 +196,76 @@ class EnvoyEnchargeBinarySensorEntity( via_device=(DOMAIN, self.envoy_serial_num), ) - super().__init__(coordinator) - - @property - def data(self) -> EnvoyData: - """Return envoy data.""" - data = self.coordinator.envoy.data - assert data is not None - return data - @property def is_on(self) -> bool: """Return the state of the Encharge binary_sensor.""" encharge_inventory = self.data.encharge_inventory assert encharge_inventory is not None return self.entity_description.value_fn(encharge_inventory[self._serial_number]) + + +class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an Enpower binary_sensor entity.""" + + entity_description: EnvoyEnpowerBinarySensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnpowerBinarySensorEntityDescription, + ) -> None: + """Init the Enpower base entity.""" + super().__init__(coordinator, description) + enpower = self.data.enpower + assert enpower is not None + self._serial_number = enpower.serial_number + self._attr_unique_id = f"{self._serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + + @property + def is_on(self) -> bool: + """Return the state of the Enpower binary_sensor.""" + enpower = self.data.enpower + assert enpower is not None + return self.entity_description.value_fn(enpower) + + +class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an Enpower dry contact binary_sensor entity.""" + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: BinarySensorEntityDescription, + relay: str, + ) -> None: + """Init the Enpower base entity.""" + super().__init__(coordinator, description) + enpower = self.data.enpower + assert enpower is not None + self.relay = self.data.dry_contact_status[relay] + self._serial_number = enpower.serial_number + self._attr_unique_id = f"{self._serial_number}_relay_{self.relay.id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + self._attr_name = ( + f"{self.data.dry_contact_settings[self.relay.id].load_name} Relay" + ) + + @property + def is_on(self) -> bool: + """Return the state of the Enpower binary_sensor.""" + return self.relay.status == DryContactStatus.CLOSED diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9656dbe9084..36faae38228 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.1.3"], + "requirements": ["pyenphase==1.2.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 37063f5e53f..019af1d393e 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -10,6 +10,7 @@ from pyenphase import ( EnvoyData, EnvoyEncharge, EnvoyEnchargePower, + EnvoyEnpower, EnvoyInverter, EnvoySystemConsumption, EnvoySystemProduction, @@ -259,6 +260,36 @@ ENCHARGE_POWER_SENSORS = ( ) +@dataclass +class EnvoyEnpowerRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnpower], datetime.datetime | int | float] + + +@dataclass +class EnvoyEnpowerSensorEntityDescription( + SensorEntityDescription, EnvoyEnpowerRequiredKeysMixin +): + """Describes an Envoy Encharge sensor entity.""" + + +ENPOWER_SENSORS = ( + EnvoyEnpowerSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda enpower: enpower.temperature, + ), + EnvoyEnpowerSensorEntityDescription( + key=LAST_REPORTED_KEY, + translation_key=LAST_REPORTED_KEY, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda enpower: dt_util.utc_from_timestamp(enpower.last_report_date), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -300,6 +331,11 @@ async def async_setup_entry( for description in ENCHARGE_POWER_SENSORS for encharge in envoy_data.encharge_power ) + if envoy_data.enpower: + entities.extend( + EnvoyEnpowerEntity(coordinator, description) + for description in ENPOWER_SENSORS + ) async_add_entities(entities) @@ -469,3 +505,38 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): encharge_power = self.data.encharge_power assert encharge_power is not None return self.entity_description.value_fn(encharge_power[self._serial_number]) + + +class EnvoyEnpowerEntity(EnvoyBaseEntity, SensorEntity): + """Envoy Enpower sensor entity.""" + + _attr_has_entity_name = True + entity_description: EnvoyEnpowerSensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnpowerSensorEntityDescription, + ) -> None: + """Initialize Enpower entity.""" + super().__init__(coordinator, description) + assert coordinator.envoy.data is not None + enpower_data = coordinator.envoy.data.enpower + assert enpower_data is not None + self._serial_number = enpower_data.serial_number + self._attr_unique_id = f"{self._serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower_data.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + + @property + def native_value(self) -> datetime.datetime | int | float | None: + """Return the state of the power sensors.""" + enpower = self.data.enpower + assert enpower is not None + return self.entity_description.value_fn(enpower) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 46ec7d9607f..915fee94e2a 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -27,10 +27,13 @@ "name": "Communicating" }, "dc_switch": { - "name": "DC Switch" + "name": "DC switch" }, "operating": { "name": "Operating" + }, + "grid_status": { + "name": "Grid status" } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index 0496849e477..7516b2e35a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.1.3 +pyenphase==1.2.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15ef1794dfe..6b1a1728fed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.1.3 +pyenphase==1.2.1 # homeassistant.components.everlights pyeverlights==0.1.0 From d569d01cfb01fbc6d079957abe387d94295101c2 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Wed, 9 Aug 2023 02:17:17 -0400 Subject: [PATCH 079/179] Bump pymazda to 0.3.11 (#98084) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index dd29d02d655..881120a0677 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pymazda"], "quality_scale": "platinum", - "requirements": ["pymazda==0.3.10"] + "requirements": ["pymazda==0.3.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7516b2e35a7..55c6ade3ebb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1824,7 +1824,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.10 +pymazda==0.3.11 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b1a1728fed..83425e2c617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.10 +pymazda==0.3.11 # homeassistant.components.melcloud pymelcloud==2.5.8 From 1b54b22a91e7f47af0bdbafbb4c058e3531a080b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Aug 2023 21:11:57 -1000 Subject: [PATCH 080/179] Bump pyenphase to 1.3.0 (#98090) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 36faae38228..7cd107a3e67 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.2.1"], + "requirements": ["pyenphase==1.3.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 55c6ade3ebb..fd0a7f14ed7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.2.1 +pyenphase==1.3.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83425e2c617..ebb87a02906 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.2.1 +pyenphase==1.3.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 35718b291795c88ae324ba6a2b724430a46cfa0e Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 9 Aug 2023 01:32:00 -0700 Subject: [PATCH 081/179] Bump opower to 0.0.24 (#98091) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 94758720722..523cbe1d988 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.20"] + "requirements": ["opower==0.0.24"] } diff --git a/requirements_all.txt b/requirements_all.txt index fd0a7f14ed7..f3daac4ff14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1365,7 +1365,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.20 +opower==0.0.24 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ebb87a02906..f0dae95b7c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1034,7 +1034,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.20 +opower==0.0.24 # homeassistant.components.oralb oralb-ble==0.17.6 From 2cae79415dcd086c3ad1ed4d9ebf8b8b2cd00b1d Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Wed, 9 Aug 2023 10:21:05 +0100 Subject: [PATCH 082/179] zha: Fix double spaces in strings.json (#98097) --- homeassistant/components/zha/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 9731fb0c2d1..3829ee68bb5 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -62,7 +62,7 @@ }, "maybe_confirm_ezsp_restore": { "title": "Overwrite Radio IEEE Address", - "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", + "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "data": { "overwrite_coordinator_ieee": "Permanently replace the radio IEEE address" } @@ -83,7 +83,7 @@ "step": { "init": { "title": "Reconfigure ZHA", - "description": "ZHA will be stopped. Do you wish to continue?" + "description": "ZHA will be stopped. Do you wish to continue?" }, "prompt_migrate_or_reconfigure": { "title": "Migrate or re-configure", @@ -95,11 +95,11 @@ }, "intent_migrate": { "title": "[%key:component::zha::options::step::prompt_migrate_or_reconfigure::menu_options::intent_migrate%]", - "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" }, "instruct_unplug": { "title": "Unplug your old radio", - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio." + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio." }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", From 7e9d0cca449b73fc94671d2bd2670cfe996a3c00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Aug 2023 01:28:27 -1000 Subject: [PATCH 083/179] Refactor enphase_envoy to have a shared base class (#98088) Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/enphase_envoy/binary_sensor.py | 38 ++-------------- .../components/enphase_envoy/entity.py | 34 ++++++++++++++ .../components/enphase_envoy/sensor.py | 45 ++++--------------- 4 files changed, 47 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/enphase_envoy/entity.py diff --git a/.coveragerc b/.coveragerc index cb9ca19c5b2..6aa0c8cce06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -304,6 +304,7 @@ omit = homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/binary_sensor.py homeassistant/components/enphase_envoy/coordinator.py + homeassistant/components/enphase_envoy/entity.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* homeassistant/components/environment_canada/__init__.py diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 4e893050b16..42778aff9d6 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -3,13 +3,8 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -import logging -from pyenphase import ( - EnvoyData, - EnvoyEncharge, - EnvoyEnpower, -) +from pyenphase import EnvoyEncharge, EnvoyEnpower from pyenphase.models.dry_contacts import DryContactStatus from homeassistant.components.binary_sensor import ( @@ -22,14 +17,10 @@ from homeassistant.const import EntityCategory 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 DOMAIN from .coordinator import EnphaseUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) +from .entity import EnvoyBaseEntity @dataclass @@ -143,32 +134,9 @@ async def async_setup_entry( async_add_entities(entities) -class EnvoyBaseBinarySensorEntity( - CoordinatorEntity[EnphaseUpdateCoordinator], BinarySensorEntity -): +class EnvoyBaseBinarySensorEntity(EnvoyBaseEntity, BinarySensorEntity): """Defines a base envoy binary_sensor entity.""" - _attr_has_entity_name = True - - def __init__( - self, - coordinator: EnphaseUpdateCoordinator, - description: BinarySensorEntityDescription, - ) -> None: - """Init the Enphase base binary_sensor entity.""" - self.entity_description = description - serial_number = coordinator.envoy.serial_number - assert serial_number is not None - self.envoy_serial_num = serial_number - super().__init__(coordinator) - - @property - def data(self) -> EnvoyData: - """Return envoy data.""" - data = self.coordinator.envoy.data - assert data is not None - return data - class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity): """Defines an Encharge binary_sensor entity.""" diff --git a/homeassistant/components/enphase_envoy/entity.py b/homeassistant/components/enphase_envoy/entity.py new file mode 100644 index 00000000000..16669bcd098 --- /dev/null +++ b/homeassistant/components/enphase_envoy/entity.py @@ -0,0 +1,34 @@ +"""Support for Enphase Envoy solar energy monitor.""" +from __future__ import annotations + +from pyenphase import EnvoyData + +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import EnphaseUpdateCoordinator + + +class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]): + """Defines a base envoy entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Init the Enphase base entity.""" + self.entity_description = description + serial_number = coordinator.envoy.serial_number + assert serial_number is not None + self.envoy_serial_num = serial_number + super().__init__(coordinator) + + @property + def data(self) -> EnvoyData: + """Return envoy data.""" + data = self.coordinator.envoy.data + assert data is not None + return data diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 019af1d393e..2b2dd591faa 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -7,7 +7,6 @@ import datetime import logging from pyenphase import ( - EnvoyData, EnvoyEncharge, EnvoyEnchargePower, EnvoyEnpower, @@ -33,13 +32,11 @@ from homeassistant.const import ( 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 homeassistant.util import dt as dt_util from .const import DOMAIN from .coordinator import EnphaseUpdateCoordinator +from .entity import EnvoyBaseEntity ICON = "mdi:flash" _LOGGER = logging.getLogger(__name__) @@ -340,34 +337,14 @@ async def async_setup_entry( async_add_entities(entities) -class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): +class EnvoySensorBaseEntity(EnvoyBaseEntity, SensorEntity): """Defines a base envoy entity.""" - def __init__( - self, - coordinator: EnphaseUpdateCoordinator, - description: SensorEntityDescription, - ) -> None: - """Init the envoy base entity.""" - self.entity_description = description - serial_number = coordinator.envoy.serial_number - assert serial_number is not None - self.envoy_serial_num = serial_number - super().__init__(coordinator) - @property - def data(self) -> EnvoyData: - """Return envoy data.""" - data = self.coordinator.envoy.data - assert data is not None - return data - - -class EnvoyEntity(EnvoyBaseEntity, SensorEntity): - """Envoy inverter entity.""" +class EnvoySystemSensorEntity(EnvoySensorBaseEntity): + """Envoy system base entity.""" _attr_icon = ICON - _attr_has_entity_name = True def __init__( self, @@ -386,7 +363,7 @@ class EnvoyEntity(EnvoyBaseEntity, SensorEntity): ) -class EnvoyProductionEntity(EnvoyEntity): +class EnvoyProductionEntity(EnvoySystemSensorEntity): """Envoy production entity.""" entity_description: EnvoyProductionSensorEntityDescription @@ -399,7 +376,7 @@ class EnvoyProductionEntity(EnvoyEntity): return self.entity_description.value_fn(system_production) -class EnvoyConsumptionEntity(EnvoyEntity): +class EnvoyConsumptionEntity(EnvoySystemSensorEntity): """Envoy consumption entity.""" entity_description: EnvoyConsumptionSensorEntityDescription @@ -412,11 +389,10 @@ class EnvoyConsumptionEntity(EnvoyEntity): return self.entity_description.value_fn(system_consumption) -class EnvoyInverterEntity(EnvoyBaseEntity, SensorEntity): +class EnvoyInverterEntity(EnvoySensorBaseEntity): """Envoy inverter entity.""" _attr_icon = ICON - _attr_has_entity_name = True entity_description: EnvoyInverterSensorEntityDescription def __init__( @@ -453,11 +429,9 @@ class EnvoyInverterEntity(EnvoyBaseEntity, SensorEntity): return self.entity_description.value_fn(inverters[self._serial_number]) -class EnvoyEnchargeEntity(EnvoyBaseEntity, SensorEntity): +class EnvoyEnchargeEntity(EnvoySensorBaseEntity): """Envoy Encharge sensor entity.""" - _attr_has_entity_name = True - def __init__( self, coordinator: EnphaseUpdateCoordinator, @@ -507,10 +481,9 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): return self.entity_description.value_fn(encharge_power[self._serial_number]) -class EnvoyEnpowerEntity(EnvoyBaseEntity, SensorEntity): +class EnvoyEnpowerEntity(EnvoySensorBaseEntity): """Envoy Enpower sensor entity.""" - _attr_has_entity_name = True entity_description: EnvoyEnpowerSensorEntityDescription def __init__( From e1f0b44ba4a425db6a72419168a899b4d448e3c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Aug 2023 14:13:57 +0200 Subject: [PATCH 084/179] Use math.isfinite instead of explicitly checking for both nan and inf (#98103) --- homeassistant/components/generic_thermostat/climate.py | 2 +- homeassistant/components/sensor/recorder.py | 2 +- homeassistant/helpers/template.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index d3d80747127..c9fcde87162 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -442,7 +442,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): """Update thermostat with latest state from sensor.""" try: cur_temp = float(state.state) - if math.isnan(cur_temp) or math.isinf(cur_temp): + if not math.isfinite(cur_temp): raise ValueError(f"Sensor has illegal state {state.state}") self._cur_temp = cur_temp except ValueError as ex: diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 2b75c1114ce..e5a35187c99 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -149,7 +149,7 @@ def _equivalent_units(units: set[str | None]) -> bool: def _parse_float(state: str) -> float: """Parse a float string, throw on inf or nan.""" fstate = float(state) - if math.isnan(fstate) or math.isinf(fstate): + if not math.isfinite(fstate): raise ValueError return fstate diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d40a0289ab8..67c1a3ed52f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1934,7 +1934,7 @@ def is_number(value): fvalue = float(value) except (ValueError, TypeError): return False - if math.isnan(fvalue) or math.isinf(fvalue): + if not math.isfinite(fvalue): return False return True From 023f2f8bb7e5c713d176d4eb733f247b0a39bfac Mon Sep 17 00:00:00 2001 From: David Knowles Date: Wed, 9 Aug 2023 09:32:50 -0400 Subject: [PATCH 085/179] Add switch platform to Schlage (#98004) * Add switch platform to Schlage * Add a generic SchlageSwitch * Use an is_on property instead of _attr_is_on * Make value_fn always return a bool --- homeassistant/components/schlage/__init__.py | 2 +- homeassistant/components/schlage/strings.json | 10 ++ homeassistant/components/schlage/switch.py | 123 ++++++++++++++++++ tests/components/schlage/conftest.py | 2 + tests/components/schlage/test_switch.py | 72 ++++++++++ 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/schlage/switch.py create mode 100644 tests/components/schlage/test_switch.py diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py index 95cfd16958c..cf95e190e88 100644 --- a/homeassistant/components/schlage/__init__.py +++ b/homeassistant/components/schlage/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN, LOGGER from .coordinator import SchlageDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/schlage/strings.json b/homeassistant/components/schlage/strings.json index 4f32ad094c0..f3612bb96b8 100644 --- a/homeassistant/components/schlage/strings.json +++ b/homeassistant/components/schlage/strings.json @@ -15,5 +15,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "switch": { + "beeper": { + "name": "Keypress Beep" + }, + "lock_and_leave": { + "name": "1-Touch Locking" + } + } } } diff --git a/homeassistant/components/schlage/switch.py b/homeassistant/components/schlage/switch.py new file mode 100644 index 00000000000..1a4eeb7bcc7 --- /dev/null +++ b/homeassistant/components/schlage/switch.py @@ -0,0 +1,123 @@ +"""Platform for Schlage switch integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial +from typing import Any + +from pyschlage.lock import Lock + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SchlageDataUpdateCoordinator +from .entity import SchlageEntity + + +@dataclass +class SchlageSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + # NOTE: This has to be a mixin because these are required keys. + # SwitchEntityDescription has attributes with default values, + # which means we can't inherit from it because you haven't have + # non-default arguments follow default arguments in an initializer. + + on_fn: Callable[[Lock], None] + off_fn: Callable[[Lock], None] + value_fn: Callable[[Lock], bool] + + +@dataclass +class SchlageSwitchEntityDescription( + SwitchEntityDescription, SchlageSwitchEntityDescriptionMixin +): + """Entity description for a Schlage switch.""" + + +SWITCHES: tuple[SchlageSwitchEntityDescription, ...] = ( + SchlageSwitchEntityDescription( + key="beeper", + translation_key="beeper", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + on_fn=lambda lock: lock.set_beeper(True), + off_fn=lambda lock: lock.set_beeper(False), + value_fn=lambda lock: lock.beeper_enabled, + ), + SchlageSwitchEntityDescription( + key="lock_and_leve", + translation_key="lock_and_leave", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + on_fn=lambda lock: lock.set_lock_and_leave(True), + off_fn=lambda lock: lock.set_lock_and_leave(False), + value_fn=lambda lock: lock.lock_and_leave_enabled, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches based on a config entry.""" + coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for device_id in coordinator.data.locks: + for description in SWITCHES: + entities.append( + SchlageSwitch( + coordinator=coordinator, + description=description, + device_id=device_id, + ) + ) + async_add_entities(entities) + + +class SchlageSwitch(SchlageEntity, SwitchEntity): + """Schlage switch entity.""" + + entity_description: SchlageSwitchEntityDescription + + def __init__( + self, + coordinator: SchlageDataUpdateCoordinator, + description: SchlageSwitchEntityDescription, + device_id: str, + ) -> None: + """Initialize a SchlageSwitch.""" + super().__init__(coordinator=coordinator, device_id=device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}_{self.entity_description.key}" + + @property + def is_on(self) -> bool: + """Return True if the switch is on.""" + return self.entity_description.value_fn(self._lock) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.hass.async_add_executor_job( + partial(self.entity_description.on_fn, self._lock) + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.hass.async_add_executor_job( + partial(self.entity_description.off_fn, self._lock) + ) + await self.coordinator.async_request_refresh() diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index c0be3d28005..0078e6a5553 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -80,6 +80,8 @@ def mock_lock(): is_jammed=False, battery_level=20, firmware_version="1.0", + lock_and_leave_enabled=True, + beeper_enabled=True, ) mock_lock.logs.return_value = [] mock_lock.last_changed_by.return_value = "thumbturn" diff --git a/tests/components/schlage/test_switch.py b/tests/components/schlage/test_switch.py new file mode 100644 index 00000000000..30e56b0686f --- /dev/null +++ b/tests/components/schlage/test_switch.py @@ -0,0 +1,72 @@ +"""Test schlage switch.""" +from unittest.mock import Mock + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +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 + + +async def test_switch_device_registry( + hass: HomeAssistant, mock_added_config_entry: ConfigEntry +) -> None: + """Test switch is added to device registry.""" + device_registry = dr.async_get(hass) + device = device_registry.async_get_device(identifiers={("schlage", "test")}) + assert device.model == "" + assert device.sw_version == "1.0" + assert device.name == "Vault Door" + assert device.manufacturer == "Schlage" + + +async def test_beeper_services( + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry +) -> None: + """Test BeeperSwitch services.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + service_data={ATTR_ENTITY_ID: "switch.vault_door_keypress_beep"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_lock.set_beeper.assert_called_once_with(False) + mock_lock.set_beeper.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "switch.vault_door_keypress_beep"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_lock.set_beeper.assert_called_once_with(True) + + await hass.config_entries.async_unload(mock_added_config_entry.entry_id) + + +async def test_lock_and_leave_services( + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry +) -> None: + """Test LockAndLeaveSwitch services.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + service_data={ATTR_ENTITY_ID: "switch.vault_door_1_touch_locking"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_lock.set_lock_and_leave.assert_called_once_with(False) + mock_lock.set_lock_and_leave.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "switch.vault_door_1_touch_locking"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_lock.set_lock_and_leave.assert_called_once_with(True) + + await hass.config_entries.async_unload(mock_added_config_entry.entry_id) From 0317afeb177ba1003728db76a6f2dc3633e5d63a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Aug 2023 15:38:57 +0200 Subject: [PATCH 086/179] Fix mock_integration and mock_platform test helpers (#98109) --- tests/common.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/common.py b/tests/common.py index 542aa0afcee..33855d4e8da 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1336,8 +1336,17 @@ def mock_integration( integration._import_platform = mock_import_platform _LOGGER.info("Adding mock integration: %s", module.DOMAIN) - hass.data.setdefault(loader.DATA_INTEGRATIONS, {})[module.DOMAIN] = integration - hass.data.setdefault(loader.DATA_COMPONENTS, {})[module.DOMAIN] = module + integration_cache = hass.data.get(loader.DATA_INTEGRATIONS) + if integration_cache is None: + integration_cache = hass.data[loader.DATA_INTEGRATIONS] = {} + loader._async_mount_config_dir(hass) + integration_cache[module.DOMAIN] = integration + + module_cache = hass.data.get(loader.DATA_COMPONENTS) + if module_cache is None: + module_cache = hass.data[loader.DATA_COMPONENTS] = {} + loader._async_mount_config_dir(hass) + module_cache[module.DOMAIN] = module return integration @@ -1361,9 +1370,16 @@ def mock_platform( platform_path is in form hue.config_flow. """ - domain, platform_name = platform_path.split(".") - integration_cache = hass.data.setdefault(loader.DATA_INTEGRATIONS, {}) - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + domain = platform_path.split(".")[0] + integration_cache = hass.data.get(loader.DATA_INTEGRATIONS) + if integration_cache is None: + integration_cache = hass.data[loader.DATA_INTEGRATIONS] = {} + loader._async_mount_config_dir(hass) + + module_cache = hass.data.get(loader.DATA_COMPONENTS) + if module_cache is None: + module_cache = hass.data[loader.DATA_COMPONENTS] = {} + loader._async_mount_config_dir(hass) if domain not in integration_cache: mock_integration(hass, MockModule(domain)) From 4c03077dfe47b9d2e64d09ce66575a17fbadd118 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 9 Aug 2023 17:20:30 +0200 Subject: [PATCH 087/179] Add product filtering feature to Trafikverket Train (#86343) --- .../components/trafikverket_train/__init__.py | 12 ++- .../trafikverket_train/config_flow.py | 58 ++++++++++++-- .../components/trafikverket_train/const.py | 1 + .../trafikverket_train/coordinator.py | 8 +- .../components/trafikverket_train/sensor.py | 12 ++- .../trafikverket_train/strings.json | 79 ++++++++++++++++--- .../trafikverket_train/test_config_flow.py | 53 +++++++++++++ 7 files changed, 203 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index 8f11590c487..a7defa2956a 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -15,7 +15,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_FROM, CONF_TO, DOMAIN, PLATFORMS +from .const import CONF_FILTER_PRODUCT, CONF_FROM, CONF_TO, DOMAIN, PLATFORMS from .coordinator import TVDataUpdateCoordinator @@ -36,7 +36,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f" {entry.data[CONF_TO]}. Error: {error} " ) from error - coordinator = TVDataUpdateCoordinator(hass, entry, to_station, from_station) + coordinator = TVDataUpdateCoordinator( + hass, entry, to_station, from_station, entry.options.get(CONF_FILTER_PRODUCT) + ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -49,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -57,3 +60,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Trafikverket Weatherstation config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index f5000851755..b7808dc38b2 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -19,7 +19,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -32,11 +32,15 @@ from homeassistant.helpers.selector import ( ) import homeassistant.util.dt as dt_util -from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN +from .const import CONF_FILTER_PRODUCT, CONF_FROM, CONF_TIME, CONF_TO, DOMAIN from .util import create_unique_id, next_departuredate _LOGGER = logging.getLogger(__name__) +OPTION_SCHEMA = { + vol.Optional(CONF_FILTER_PRODUCT, default=""): TextSelector(), +} + DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): TextSelector(), @@ -52,7 +56,7 @@ DATA_SCHEMA = vol.Schema( ) ), } -) +).extend(OPTION_SCHEMA) DATA_SCHEMA_REAUTH = vol.Schema( { vol.Required(CONF_API_KEY): cv.string, @@ -67,6 +71,7 @@ async def validate_input( train_to: str, train_time: str | None, weekdays: list[str], + product_filter: str | None, ) -> dict[str, str]: """Validate input from user input.""" errors: dict[str, str] = {} @@ -87,9 +92,13 @@ async def validate_input( from_station = await train_api.async_get_train_station(train_from) to_station = await train_api.async_get_train_station(train_to) if train_time: - await train_api.async_get_train_stop(from_station, to_station, when) + await train_api.async_get_train_stop( + from_station, to_station, when, product_filter + ) else: - await train_api.async_get_next_train_stop(from_station, to_station, when) + await train_api.async_get_next_train_stop( + from_station, to_station, when, product_filter + ) except InvalidAuthentication: errors["base"] = "invalid_auth" except NoTrainStationFound: @@ -117,6 +126,14 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): entry: config_entries.ConfigEntry | None + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> TVTrainOptionsFlowHandler: + """Get the options flow for this handler.""" + return TVTrainOptionsFlowHandler(config_entry) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" @@ -140,6 +157,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry.data[CONF_TO], self.entry.data.get(CONF_TIME), self.entry.data[CONF_WEEKDAY], + self.entry.options.get(CONF_FILTER_PRODUCT), ) if not errors: self.hass.config_entries.async_update_entry( @@ -170,6 +188,10 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): train_to: str = user_input[CONF_TO] train_time: str | None = user_input.get(CONF_TIME) train_days: list = user_input[CONF_WEEKDAY] + filter_product: str | None = user_input[CONF_FILTER_PRODUCT] + + if filter_product == "": + filter_product = None name = f"{train_from} to {train_to}" if train_time: @@ -182,6 +204,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): train_to, train_time, train_days, + filter_product, ) if not errors: unique_id = create_unique_id( @@ -199,6 +222,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_TIME: train_time, CONF_WEEKDAY: train_days, }, + options={CONF_FILTER_PRODUCT: filter_product}, ) return self.async_show_form( @@ -208,3 +232,27 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + +class TVTrainOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): + """Handle Trafikverket Train options.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage Trafikverket Train options.""" + errors: dict[str, Any] = {} + + if user_input: + if not (_filter := user_input.get(CONF_FILTER_PRODUCT)) or _filter == "": + user_input[CONF_FILTER_PRODUCT] = None + return self.async_create_entry(data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=self.add_suggested_values_to_schema( + vol.Schema(OPTION_SCHEMA), + user_input or self.options, + ), + errors=errors, + ) diff --git a/homeassistant/components/trafikverket_train/const.py b/homeassistant/components/trafikverket_train/const.py index 253383b4b5a..e1852ce9ada 100644 --- a/homeassistant/components/trafikverket_train/const.py +++ b/homeassistant/components/trafikverket_train/const.py @@ -8,3 +8,4 @@ ATTRIBUTION = "Data provided by Trafikverket" CONF_FROM = "from" CONF_TO = "to" CONF_TIME = "time" +CONF_FILTER_PRODUCT = "filter_product" diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py index fac1c418b09..ea852ab7fdf 100644 --- a/homeassistant/components/trafikverket_train/coordinator.py +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -39,6 +39,7 @@ class TrainData: actual_time: datetime | None other_info: str | None deviation: str | None + product_filter: str | None _LOGGER = logging.getLogger(__name__) @@ -68,6 +69,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): entry: ConfigEntry, to_station: StationInfo, from_station: StationInfo, + filter_product: str | None, ) -> None: """Initialize the Trafikverket coordinator.""" super().__init__( @@ -83,6 +85,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): self.to_station: StationInfo = to_station self._time: time | None = dt_util.parse_time(entry.data[CONF_TIME]) self._weekdays: list[str] = entry.data[CONF_WEEKDAY] + self._filter_product = filter_product async def _async_update_data(self) -> TrainData: """Fetch data from Trafikverket.""" @@ -99,11 +102,11 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): try: if self._time: state = await self._train_api.async_get_train_stop( - self.from_station, self.to_station, when + self.from_station, self.to_station, when, self._filter_product ) else: state = await self._train_api.async_get_next_train_stop( - self.from_station, self.to_station, when + self.from_station, self.to_station, when, self._filter_product ) except InvalidAuthentication as error: raise ConfigEntryAuthFailed from error @@ -134,6 +137,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): actual_time=_get_as_utc(state.time_at_location), other_info=_get_as_joined(state.other_information), deviation=_get_as_joined(state.deviations), + product_filter=self._filter_product, ) return states diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 97d7a6b34fa..b5f993073a5 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -1,9 +1,10 @@ """Train information for departures and delays, provided by Trafikverket.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import datetime +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,6 +23,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN from .coordinator import TrainData, TVDataUpdateCoordinator +ATTR_PRODUCT_FILTER = "product_filter" + @dataclass class TrafikverketRequiredKeysMixin: @@ -158,3 +161,10 @@ class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): def _handle_coordinator_update(self) -> None: self._update_attr() return super()._handle_coordinator_update() + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional attributes for Trafikverket Train sensor.""" + if self.coordinator.data.product_filter: + return {ATTR_PRODUCT_FILTER: self.coordinator.data.product_filter} + return None diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json index aabab0907ab..78d69c880ae 100644 --- a/homeassistant/components/trafikverket_train/strings.json +++ b/homeassistant/components/trafikverket_train/strings.json @@ -20,10 +20,12 @@ "to": "To station", "from": "From station", "time": "Time (optional)", - "weekday": "Days" + "weekday": "Days", + "filter_product": "Filter by product description" }, "data_description": { - "time": "Set time to search specifically at this time of day, must be exact time as scheduled train departure" + "time": "Set time to search specifically at this time of day, must be exact time as scheduled train departure", + "filter_product": "To filter by product description add the phrase here to match" } }, "reauth_confirm": { @@ -33,6 +35,18 @@ } } }, + "options": { + "step": { + "init": { + "data": { + "filter_product": "[%key:component::trafikverket_train::config::step::user::data::filter_product%]" + }, + "data_description": { + "filter_product": "[%key:component::trafikverket_train::config::step::user::data_description::filter_product%]" + } + } + } + }, "selector": { "weekday": { "options": { @@ -49,7 +63,12 @@ "entity": { "sensor": { "departure_time": { - "name": "Departure time" + "name": "Departure time", + "state_attributes": { + "product_filter": { + "name": "Train filtering" + } + } }, "departure_state": { "name": "Departure state", @@ -57,28 +76,68 @@ "on_time": "On time", "delayed": "Delayed", "canceled": "Cancelled" + }, + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } } }, "cancelled": { - "name": "Cancelled" + "name": "Cancelled", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } }, "delayed_time": { - "name": "Delayed time" + "name": "Delayed time", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } }, "planned_time": { - "name": "Planned time" + "name": "Planned time", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } }, "estimated_time": { - "name": "Estimated time" + "name": "Estimated time", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } }, "actual_time": { - "name": "Actual time" + "name": "Actual time", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } }, "other_info": { - "name": "Other information" + "name": "Other information", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } }, "deviation": { - "name": "Deviation" + "name": "Deviation", + "state_attributes": { + "product_filter": { + "name": "[%key:component::trafikverket_train::entity::sensor::departure_time::state_attributes::product_filter::name%]" + } + } } } } diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index a3b449755c7..3493e031669 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -66,6 +66,7 @@ async def test_form(hass: HomeAssistant) -> None: "time": "10:00", "weekday": ["mon", "fri"], } + assert result["options"] == {"filter_product": None} assert len(mock_setup_entry.mock_calls) == 1 assert result["result"].unique_id == "{}-{}-{}-{}".format( "stockholmc", "uppsalac", "10:00", "['mon', 'fri']" @@ -448,3 +449,55 @@ async def test_reauth_flow_error_departures( "time": "10:00", "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], } + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C at 10:00", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ): + assert 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"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"filter_product": "SJ Regionaltåg"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == {"filter_product": "SJ Regionaltåg"} + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"filter_product": ""}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == {"filter_product": None} From 138854a9ccf1487bc4a2a0ab4eb5301340e1d5fa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 9 Aug 2023 17:44:08 +0200 Subject: [PATCH 088/179] Migrate EAFM to has entity name (#98121) --- homeassistant/components/eafm/sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index ce3ee2bfbec..d673c562bbb 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -95,6 +95,8 @@ class Measurement(CoordinatorEntity, SensorEntity): "from the real-time data API" ) _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True + _attr_name = None def __init__(self, coordinator, key): """Initialise the gauge with a data instance and station.""" @@ -122,11 +124,6 @@ class Measurement(CoordinatorEntity, SensorEntity): """Return the parameter name for the station.""" return self.coordinator.data["measures"][self.key]["parameterName"] - @property - def name(self): - """Return the name of the gauge.""" - return f"{self.station_name} {self.parameter_name} {self.qualifier}" - @property def device_info(self): """Return the device info.""" @@ -135,7 +132,7 @@ class Measurement(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, "measure-id", self.station_id)}, manufacturer="https://environment.data.gov.uk/", model=self.parameter_name, - name=self.name, + name=f"{self.station_name} {self.parameter_name} {self.qualifier}", ) @property From 02c27d8ad27e1ca94020b96d77cd1a491c253edd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Aug 2023 18:52:35 +0200 Subject: [PATCH 089/179] UniFi WLAN availability affected by WLAN enabled (#98020) --- homeassistant/components/unifi/entity.py | 7 ++++++ homeassistant/components/unifi/image.py | 5 ++-- homeassistant/components/unifi/sensor.py | 9 ++++--- tests/components/unifi/test_image.py | 32 +++++++++++++++++++++--- tests/components/unifi/test_sensor.py | 13 ++++++++++ 5 files changed, 56 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index db7b414b3b0..7d9373d1188 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -44,6 +44,13 @@ def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: return controller.available and not device.disabled +@callback +def async_wlan_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if WLAN is available.""" + wlan = controller.api.wlans[obj_id] + return controller.available and wlan.enabled + + @callback def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for device.""" diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index c3969c21bc4..3ff893838c9 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -26,6 +26,7 @@ from .entity import ( HandlerT, UnifiEntity, UnifiEntityDescription, + async_wlan_available_fn, async_wlan_device_info_fn, ) @@ -61,11 +62,11 @@ ENTITY_DESCRIPTIONS: tuple[UnifiImageEntityDescription, ...] = ( entity_registry_enabled_default=False, allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.wlans, - available_fn=lambda controller, _: controller.available, + available_fn=async_wlan_available_fn, device_info_fn=async_wlan_device_info_fn, event_is_on=None, event_to_subscribe=None, - name_fn=lambda _: "QR Code", + name_fn=lambda wlan: "QR Code", object_fn=lambda api, obj_id: api.wlans[obj_id], should_poll=False, supported_fn=lambda controller, obj_id: True, diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index bb9365d486b..23cc8724c2c 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -39,6 +39,7 @@ from .entity import ( async_client_device_info_fn, async_device_available_fn, async_device_device_info_fn, + async_wlan_available_fn, async_wlan_device_info_fn, ) @@ -179,16 +180,16 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( key="WLAN clients", entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=True, - allowed_fn=lambda controller, _: True, + allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.wlans, - available_fn=lambda controller, obj_id: controller.available, + available_fn=async_wlan_available_fn, device_info_fn=async_wlan_device_info_fn, event_is_on=None, event_to_subscribe=None, - name_fn=lambda client: None, + name_fn=lambda wlan: None, object_fn=lambda api, obj_id: api.wlans[obj_id], should_poll=True, - supported_fn=lambda controller, _: True, + supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"wlan_clients-{obj_id}", value_fn=async_wlan_client_value_fn, ), diff --git a/tests/components/unifi/test_image.py b/tests/components/unifi/test_image.py index 564fd7598d8..afefee9fd02 100644 --- a/tests/components/unifi/test_image.py +++ b/tests/components/unifi/test_image.py @@ -5,13 +5,12 @@ from datetime import timedelta from http import HTTPStatus from aiounifi.models.message import MessageKey +from aiounifi.websocket import WebsocketState from syrupy.assertion import SnapshotAssertion from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import ( - EntityCategory, -) +from homeassistant.const import STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler @@ -106,7 +105,7 @@ async def test_wlan_qr_code( image_state_2 = hass.states.get("image.ssid_1_qr_code") assert image_state_1.state == image_state_2.state - # Update state object - changeed password - new state + # Update state object - changed password - new state data = deepcopy(WLAN) data["x_passphrase"] = "new password" mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=data) @@ -120,3 +119,28 @@ async def test_wlan_qr_code( assert resp.status == HTTPStatus.OK body = await resp.read() assert body == snapshot + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert hass.states.get("image.ssid_1_qr_code").state == STATE_UNAVAILABLE + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE + + # WLAN gets disabled + wlan_1 = deepcopy(WLAN) + wlan_1["enabled"] = False + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + await hass.async_block_till_done() + assert hass.states.get("image.ssid_1_qr_code").state == STATE_UNAVAILABLE + + # WLAN gets re-enabled + wlan_1["enabled"] = True + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + await hass.async_block_till_done() + assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3d50df8ada9..9670ecb43d0 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -558,3 +558,16 @@ async def test_wlan_client_sensors( mock_unifi_websocket(state=WebsocketState.RUNNING) await hass.async_block_till_done() assert hass.states.get("sensor.ssid_1").state == "0" + + # WLAN gets disabled + wlan_1 = deepcopy(WLAN) + wlan_1["enabled"] = False + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.ssid_1").state == STATE_UNAVAILABLE + + # WLAN gets re-enabled + wlan_1["enabled"] = True + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.ssid_1").state == "0" From 2841cbbed272114c97afa2d0b36f30dca6b564d8 Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 9 Aug 2023 16:04:01 -0400 Subject: [PATCH 090/179] Add Off-peak power control to Roborock (#97307) * add off-peak switch and time * Make off_peak disabled by default --- .../components/roborock/strings.json | 9 +++++ homeassistant/components/roborock/switch.py | 19 ++++++++++ homeassistant/components/roborock/time.py | 38 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index cd629e208e3..5ca2292f804 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -150,6 +150,9 @@ "dnd_switch": { "name": "Do not disturb" }, + "off_peak_switch": { + "name": "Off-peak charging" + }, "status_indicator": { "name": "Status indicator light" } @@ -160,6 +163,12 @@ }, "dnd_end_time": { "name": "Do not disturb end" + }, + "off_peak_start_time": { + "name": "Off-peak start" + }, + "off_peak_end_time": { + "name": "Off-peak end" } }, "vacuum": { diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 312753ced01..de820ede8fa 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -84,6 +84,25 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ icon="mdi:bell-cancel", entity_category=EntityCategory.CONFIG, ), + RoborockSwitchDescription( + cache_key=CacheableAttribute.valley_electricity_timer, + update_value=lambda cache, value: cache.update_value( + [ + cache.value.get("start_hour"), + cache.value.get("start_minute"), + cache.value.get("end_hour"), + cache.value.get("end_minute"), + ] + ) + if value + else cache.close_value(), + attribute="enabled", + key="off_peak_switch", + translation_key="off_peak_switch", + icon="mdi:power-plug", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), ] diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 514d147d469..5dc98e09352 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -79,6 +79,44 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [ ), entity_category=EntityCategory.CONFIG, ), + RoborockTimeDescription( + key="off_peak_start", + translation_key="off_peak_start", + icon="mdi:power-plug", + cache_key=CacheableAttribute.valley_electricity_timer, + update_value=lambda cache, desired_time: cache.update_value( + [ + desired_time.hour, + desired_time.minute, + cache.value.get("end_hour"), + cache.value.get("end_minute"), + ] + ), + get_value=lambda cache: datetime.time( + hour=cache.value.get("start_hour"), minute=cache.value.get("start_minute") + ), + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + RoborockTimeDescription( + key="off_peak_end", + translation_key="off_peak_end", + icon="mdi:power-plug-off", + cache_key=CacheableAttribute.valley_electricity_timer, + update_value=lambda cache, desired_time: cache.update_value( + [ + cache.value.get("start_hour"), + cache.value.get("start_minute"), + desired_time.hour, + desired_time.minute, + ] + ), + get_value=lambda cache: datetime.time( + hour=cache.value.get("end_hour"), minute=cache.value.get("end_minute") + ), + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), ] From 5d3d66e47d066c74d596a326631165dea8411081 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 10 Aug 2023 01:28:08 -0400 Subject: [PATCH 091/179] Bump zwave-js-server-python to 0.50.1 (#94760) * Bump zwave-js-server-python to 0.50.0 * handle additional upstream changes * Additional changes * fix tests * Convert two similar functions to be one function * Fix docstring * Remove conditional pydantic import * Revert scope change * Bump zwave-js-server-python * Set default return value for command * Remove line breaks * Add coverage --- homeassistant/components/zwave_js/__init__.py | 6 +-- homeassistant/components/zwave_js/api.py | 1 + homeassistant/components/zwave_js/climate.py | 5 +- .../components/zwave_js/device_action.py | 5 +- .../zwave_js/device_automation_helpers.py | 23 -------- .../components/zwave_js/device_condition.py | 4 +- .../components/zwave_js/diagnostics.py | 30 +++++++---- .../components/zwave_js/discovery.py | 53 ++++++++----------- homeassistant/components/zwave_js/entity.py | 14 +++-- homeassistant/components/zwave_js/helpers.py | 9 ++-- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/services.py | 30 +++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/conftest.py | 3 ++ .../nortek_thermostat_removed_event.json | 2 +- tests/components/zwave_js/test_api.py | 8 +-- tests/components/zwave_js/test_climate.py | 3 ++ tests/components/zwave_js/test_cover.py | 4 ++ tests/components/zwave_js/test_diagnostics.py | 14 ++++- tests/components/zwave_js/test_discovery.py | 2 + tests/components/zwave_js/test_fan.py | 6 +++ tests/components/zwave_js/test_helpers.py | 14 +++++ tests/components/zwave_js/test_init.py | 24 ++++----- tests/components/zwave_js/test_services.py | 12 +++-- tests/components/zwave_js/test_switch.py | 2 + tests/components/zwave_js/test_trigger.py | 4 +- tests/components/zwave_js/test_update.py | 15 ++++-- 28 files changed, 171 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 7ff351893b1..d477964d229 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -9,7 +9,7 @@ from typing import Any from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass +from zwave_js_server.const import CommandClass, RemoveNodeReason from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode @@ -398,13 +398,13 @@ class ControllerEvents: def async_on_node_removed(self, event: dict) -> None: """Handle node removed event.""" node: ZwaveNode = event["node"] - replaced: bool = event.get("replaced", False) + reason: RemoveNodeReason = event["reason"] # grab device in device registry attached to this node dev_id = get_device_id(self.driver_events.driver, node) device = self.dev_reg.async_get_device(identifiers={dev_id}) # We assert because we know the device exists assert device - if replaced: + if reason in (RemoveNodeReason.REPLACED, RemoveNodeReason.PROXY_REPLACED): self.discovered_value_ids.pop(device.id, None) async_dispatcher_send( diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5fc7da68e99..6d2461df3e4 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1138,6 +1138,7 @@ async def websocket_remove_node( node = event["node"] node_details = { "node_id": node.node_id, + "reason": event["reason"], } connection.send_message( diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 327db05cb00..d511a030fb1 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -507,8 +507,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): # Please use Dry and Fan HVAC modes instead. if preset_mode_value in (ThermostatMode.DRY, ThermostatMode.FAN): LOGGER.warning( - "Dry and Fan preset modes are deprecated and will be removed in Home Assistant 2024.2. " - "Please use the corresponding Dry and Fan HVAC modes instead" + "Dry and Fan preset modes are deprecated and will be removed in Home " + "Assistant 2024.2. Please use the corresponding Dry and Fan HVAC " + "modes instead" ) async_create_issue( self.hass, diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 04db33fdff6..b9b0c3a6e86 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -54,9 +54,8 @@ from .device_automation_helpers import ( CONF_SUBTYPE, VALUE_ID_REGEX, generate_config_parameter_subtype, - get_config_parameter_value_schema, ) -from .helpers import async_get_node_from_device_id +from .helpers import async_get_node_from_device_id, get_value_state_schema ACTION_TYPES = { SERVICE_CLEAR_LOCK_USERCODE, @@ -357,7 +356,7 @@ async def async_get_action_capabilities( property_key=config[ATTR_CONFIG_PARAMETER_BITMASK], endpoint=config[ATTR_ENDPOINT], ) - value_schema = get_config_parameter_value_schema(node, value_id) + value_schema = get_value_state_schema(node.values[value_id]) if value_schema is None: return {} return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})} diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 7a60d491b3c..2c375485e6b 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -1,12 +1,7 @@ """Provides helpers for Z-Wave JS device automations.""" from __future__ import annotations -from typing import cast - -import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import ConfigurationValueType -from zwave_js_server.model.node import Node from zwave_js_server.model.value import ConfigurationValue from homeassistant.config_entries import ConfigEntryState @@ -23,24 +18,6 @@ CONF_VALUE_ID = "value_id" VALUE_ID_REGEX = r"([0-9]+-[0-9]+-[0-9]+-).+" -def get_config_parameter_value_schema(node: Node, value_id: str) -> vol.Schema | None: - """Get the extra fields schema for a config parameter value.""" - 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, - ): - return vol.All(vol.Coerce(int), vol.Range(min=min_, max=max_)) - - if config_value.configuration_value_type == ConfigurationValueType.ENUMERATED: - return vol.In({int(k): v for k, v in config_value.metadata.states.items()}) - - return None - - def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str: """Generate the config parameter name used in a device automation subtype.""" parameter = str(config_value.property_) diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 3e089362d0b..26b4c637b6e 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -31,11 +31,11 @@ from .device_automation_helpers import ( NODE_STATUSES, async_bypass_dynamic_config_validation, generate_config_parameter_subtype, - get_config_parameter_value_schema, ) from .helpers import ( async_get_node_from_device_id, check_type_schema_map, + get_value_state_schema, get_zwave_value_from_config, remove_keys_with_empty_values, ) @@ -209,7 +209,7 @@ async def async_get_condition_capabilities( # Add additional fields to the automation trigger UI if config[CONF_TYPE] == CONFIG_PARAMETER_TYPE: value_id = config[CONF_VALUE_ID] - value_schema = get_config_parameter_value_schema(node, value_id) + value_schema = get_value_state_schema(node.values[value_id]) if value_schema is None: return {} return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})} diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 2fe2b17fe1b..afae214ab2b 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -7,8 +7,9 @@ from typing import Any from zwave_js_server.client import Client from zwave_js_server.const import CommandClass from zwave_js_server.dump import dump_msgs -from zwave_js_server.model.node import Node, NodeDataType +from zwave_js_server.model.node import Node from zwave_js_server.model.value import ValueDataType +from zwave_js_server.util.node import dump_node_state from homeassistant.components.diagnostics import REDACTED from homeassistant.components.diagnostics.util import async_redact_data @@ -54,13 +55,20 @@ def optionally_redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueD return zwave_value -def redact_node_state(node_state: NodeDataType) -> NodeDataType: +def redact_node_state(node_state: dict) -> dict: """Redact node state.""" - redacted_state: NodeDataType = deepcopy(node_state) - redacted_state["values"] = [ - optionally_redact_value_of_zwave_value(zwave_value) - for zwave_value in node_state["values"] - ] + redacted_state: dict = deepcopy(node_state) + # dump_msgs returns values in a list but dump_node_state returns them in a dict + if isinstance(node_state["values"], list): + redacted_state["values"] = [ + optionally_redact_value_of_zwave_value(zwave_value) + for zwave_value in node_state["values"] + ] + else: + redacted_state["values"] = { + value_id: optionally_redact_value_of_zwave_value(zwave_value) + for value_id, zwave_value in node_state["values"].items() + } return redacted_state @@ -129,8 +137,8 @@ async def async_get_config_entry_diagnostics( handshake_msgs = msgs[:-1] network_state = msgs[-1] network_state["result"]["state"]["nodes"] = [ - redact_node_state(async_redact_data(node, KEYS_TO_REDACT)) - for node in network_state["result"]["state"]["nodes"] + redact_node_state(async_redact_data(node_data, KEYS_TO_REDACT)) + for node_data in network_state["result"]["state"]["nodes"] ] return {"messages": [*handshake_msgs, network_state]} @@ -148,7 +156,9 @@ async def async_get_device_diagnostics( node = driver.controller.nodes[node_id] entities = get_device_entities(hass, node, config_entry, device) assert client.version - node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)) + node_state = redact_node_state( + async_redact_data(dump_node_state(node), KEYS_TO_REDACT) + ) return { "versionInfo": { "driverVersion": client.version.driver_version, diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 9569ba97167..c879cc1f5b4 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1108,7 +1108,7 @@ def async_discover_single_value( def async_discover_single_configuration_value( value: ConfigurationValue, ) -> Generator[ZwaveDiscoveryInfo, None, None]: - """Run discovery on a single ZWave configuration value and return matching schema info.""" + """Run discovery on single Z-Wave configuration value and return schema matches.""" if value.metadata.writeable and value.metadata.readable: if value.configuration_value_type == ConfigurationValueType.ENUMERATED: yield ZwaveDiscoveryInfo( @@ -1125,36 +1125,29 @@ def async_discover_single_configuration_value( ConfigurationValueType.RANGE, ConfigurationValueType.MANUAL_ENTRY, ): - if value.metadata.type == ValueType.BOOLEAN or ( - value.metadata.min == 0 and value.metadata.max == 1 - ): - yield ZwaveDiscoveryInfo( - node=value.node, - primary_value=value, - assumed_state=False, - platform=Platform.SWITCH, - platform_hint="config_parameter", - platform_data=None, - additional_value_ids_to_watch=set(), - entity_registry_enabled_default=False, - ) - else: - yield ZwaveDiscoveryInfo( - node=value.node, - primary_value=value, - assumed_state=False, - platform=Platform.NUMBER, - platform_hint="config_parameter", - platform_data=None, - additional_value_ids_to_watch=set(), - entity_registry_enabled_default=False, - ) + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.NUMBER, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) + elif value.configuration_value_type == ConfigurationValueType.BOOLEAN: + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + assumed_state=False, + platform=Platform.SWITCH, + platform_hint="config_parameter", + platform_data=None, + additional_value_ids_to_watch=set(), + entity_registry_enabled_default=False, + ) elif not value.metadata.writeable and value.metadata.readable: - if value.metadata.type == ValueType.BOOLEAN or ( - value.metadata.min == 0 - and value.metadata.max == 1 - and not value.metadata.states - ): + if value.configuration_value_type == ConfigurationValueType.BOOLEAN: yield ZwaveDiscoveryInfo( node=value.node, primary_value=value, diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 2a0f5ff4e72..6cf2a402f3f 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -7,7 +7,11 @@ from typing import Any from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError from zwave_js_server.model.driver import Driver -from zwave_js_server.model.value import Value as ZwaveValue, get_value_id_str +from zwave_js_server.model.value import ( + SetValueResult, + Value as ZwaveValue, + get_value_id_str, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback @@ -70,9 +74,9 @@ class ZWaveBaseEntity(Entity): async def _async_poll_value(self, value_or_id: str | ZwaveValue) -> None: """Poll a value.""" - # We log an error instead of raising an exception because this service call occurs - # in a separate task and we don't want to raise the exception in that separate task - # because it is confusing to the user. + # We log an error instead of raising an exception because this service call + # occurs in a separate task and we don't want to raise the exception in that + # separate task because it is confusing to the user. try: await self.info.node.async_poll_value(value_or_id) except BaseZwaveJSServerError as err: @@ -312,7 +316,7 @@ class ZWaveBaseEntity(Entity): new_value: Any, options: dict | None = None, wait_for_result: bool | None = None, - ) -> bool | None: + ) -> SetValueResult | None: """Set value on node.""" try: return await self.info.node.async_set_value( diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 6c54a464837..adce141f91c 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -252,7 +252,7 @@ def async_get_node_from_entity_id( entity_entry = ent_reg.async_get(entity_id) if entity_entry is None or entity_entry.platform != DOMAIN: - raise ValueError(f"Entity {entity_id} is not a valid {DOMAIN} entity.") + raise ValueError(f"Entity {entity_id} is not a valid {DOMAIN} entity") # Assert for mypy, safe because we know that zwave_js entities are always # tied to a device @@ -414,9 +414,7 @@ def copy_available_params( ) -def get_value_state_schema( - value: ZwaveValue, -) -> vol.Schema | None: +def get_value_state_schema(value: ZwaveValue) -> vol.Schema | None: """Return device automation schema for a config entry.""" if isinstance(value, ConfigurationValue): min_ = value.metadata.min @@ -427,6 +425,9 @@ def get_value_state_schema( ): return vol.All(vol.Coerce(int), vol.Range(min=min_, max=max_)) + if value.configuration_value_type == ConfigurationValueType.BOOLEAN: + return vol.Coerce(bool) + if value.configuration_value_type == ConfigurationValueType.ENUMERATED: return vol.In({int(k): v for k, v in value.metadata.states.items()}) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index b163ace1d24..43dddd08a1a 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.49.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.50.1"], "usb": [ { "vid": "0658", diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 133cb407405..44ef3a2269c 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -8,7 +8,7 @@ from typing import Any import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass, CommandStatus +from zwave_js_server.const import SET_VALUE_SUCCESS, CommandClass, CommandStatus from zwave_js_server.exceptions import FailedZWaveCommand, SetValueFailed from zwave_js_server.model.endpoint import Endpoint from zwave_js_server.model.node import Node as ZwaveNode @@ -39,12 +39,6 @@ from .helpers import ( _LOGGER = logging.getLogger(__name__) -SET_VALUE_FAILED_EXC = SetValueFailed( - "Unable to set value, refer to " - "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue for " - "possible reasons" -) - def parameter_name_does_not_need_bitmask( val: dict[str, int | str | list[str]] @@ -538,16 +532,20 @@ class ZWaveServices: nodes_list = list(nodes) # multiple set_values my fail so we will track the entire list set_value_failed_nodes_list: list[ZwaveNode | Endpoint] = [] - for node_, success in get_valid_responses_from_results(nodes_list, results): - if success is False: - # If we failed to set a value, add node to SetValueFailed exception list + set_value_failed_error_list: list[SetValueFailed] = [] + for node_, result in get_valid_responses_from_results(nodes_list, results): + if result and result.status not in SET_VALUE_SUCCESS: + # If we failed to set a value, add node to exception list set_value_failed_nodes_list.append(node_) + set_value_failed_error_list.append( + SetValueFailed(f"{result.status} {result.message}") + ) - # Add the SetValueFailed exception to the results and the nodes to the node - # list. No-op if there are no SetValueFailed exceptions + # Add the exception to the results and the nodes to the node list. No-op if + # no set value commands failed raise_exceptions_from_results( (*nodes_list, *set_value_failed_nodes_list), - (*results, *([SET_VALUE_FAILED_EXC] * len(set_value_failed_nodes_list))), + (*results, *set_value_failed_error_list), ) async def async_multicast_set_value(self, service: ServiceCall) -> None: @@ -611,7 +609,7 @@ class ZWaveServices: new_value = str(new_value) try: - success = await async_multicast_set_value( + result = await async_multicast_set_value( client=client, new_value=new_value, value_data=value, @@ -621,10 +619,10 @@ class ZWaveServices: except FailedZWaveCommand as err: raise HomeAssistantError("Unable to set value via multicast") from err - if success is False: + if result.status not in SET_VALUE_SUCCESS: raise HomeAssistantError( "Unable to set value via multicast" - ) from SetValueFailed + ) from SetValueFailed(f"{result.status} {result.message}") async def async_ping(self, service: ServiceCall) -> None: """Ping node(s).""" diff --git a/requirements_all.txt b/requirements_all.txt index f3daac4ff14..be5cbbfdf6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ zigpy==0.56.4 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.49.0 +zwave-js-server-python==0.50.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0dae95b7c1..8eb00604a77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2049,7 +2049,7 @@ zigpy-znp==0.11.4 zigpy==0.56.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.49.0 +zwave-js-server-python==0.50.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 0eb4ec775f9..8bb55e3949b 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -687,6 +687,9 @@ def mock_client_fixture( client.version = VersionInfo.from_message(version_state) client.ws_server_url = "ws://test:3000/zjs" + client.async_send_command.return_value = { + "result": {"success": True, "status": 255} + } yield client diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json index 8491e65c037..e30e0297e7d 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json @@ -270,5 +270,5 @@ } ] }, - "replaced": false + "reason": 0 } diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index ebdf2112435..5bafe932362 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -276,14 +276,16 @@ async def test_subscribe_node_status( msg = await ws_client.receive_json() assert msg["success"] - node.data["ready"] = True + new_node_data = deepcopy(multisensor_6_state) + new_node_data["ready"] = True + event = Event( "ready", { "source": "node", "event": "ready", "nodeId": node.node_id, - "nodeState": node.data, + "nodeState": new_node_data, }, ) node.receive_event(event) @@ -1715,7 +1717,7 @@ async def test_remove_node( assert len(client.async_send_command.call_args_list) == 1 assert client.async_send_command.call_args[0][0] == { "command": "controller.begin_exclusion", - "strategy": 0, + "options": {"strategy": 0}, } # Test FailedZWaveCommand is caught diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 23d34c131b8..e9040dfd397 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -731,6 +731,8 @@ async def test_thermostat_raise_repair_issue_and_warning_when_setting_dry_preset caplog: pytest.LogCaptureFixture, ) -> None: """Test raise of repair issue and warning when setting Dry preset.""" + client.async_send_command.return_value = {"result": {"status": 1}} + state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) assert state @@ -765,6 +767,7 @@ async def test_thermostat_raise_repair_issue_and_warning_when_setting_fan_preset caplog: pytest.LogCaptureFixture, ) -> None: """Test raise of repair issue and warning when setting Fan preset.""" + client.async_send_command.return_value = {"result": {"status": 1}} state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) assert state diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 502f2413c99..e51b3751ac8 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -126,6 +126,7 @@ async def test_window_cover( assert args["value"] client.async_send_command.reset_mock() + # Test stop after opening await hass.services.async_call( DOMAIN, @@ -265,6 +266,7 @@ async def test_fibaro_fgr222_shutter_cover( assert args["value"] == 99 client.async_send_command.reset_mock() + # Test closing tilts await hass.services.async_call( DOMAIN, @@ -286,6 +288,7 @@ async def test_fibaro_fgr222_shutter_cover( assert args["value"] == 0 client.async_send_command.reset_mock() + # Test setting tilt position await hass.services.async_call( DOMAIN, @@ -365,6 +368,7 @@ async def test_aeotec_nano_shutter_cover( assert args["value"] client.async_send_command.reset_mock() + # Test stop after opening await hass.services.async_call( DOMAIN, diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 4454e38e0d8..2510143695c 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -125,7 +125,13 @@ async def test_device_diagnostics( entity["entity_id"] == "test.unrelated_entity" for entity in diagnostics_data["entities"] ) - assert diagnostics_data["state"] == multisensor_6.data + assert diagnostics_data["state"] == { + **multisensor_6.data, + "values": {id: val.data for id, val in multisensor_6.values.items()}, + "endpoints": { + str(idx): endpoint.data for idx, endpoint in multisensor_6.endpoints.items() + }, + } async def test_device_diagnostics_error(hass: HomeAssistant, integration) -> None: @@ -230,7 +236,11 @@ async def test_device_diagnostics_secret_value( """Find ultraviolet property value in data.""" return next( val - for val in data["values"] + for val in ( + data["values"] + if isinstance(data["values"], list) + else data["values"].values() + ) if val["commandClass"] == CommandClass.SENSOR_MULTILEVEL and val["property"] == PROPERTY_ULTRAVIOLET ) diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 1c4a69d32e3..99a46eaadf9 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -171,6 +171,7 @@ async def test_zooz_zen72( state = hass.states.get(entity_id) assert state assert state.state == STATE_UNKNOWN + await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -256,6 +257,7 @@ async def test_indicator_test( state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF + await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 2b508700413..92141eec3ff 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -231,6 +231,7 @@ async def test_configurable_speeds_fan( async def get_zwave_speed_from_percentage(percentage): """Set the fan to a particular percentage and get the resulting Zwave speed.""" client.async_send_command.reset_mock() + await hass.services.async_call( "fan", "turn_on", @@ -356,6 +357,7 @@ async def test_ge_12730_fan(hass: HomeAssistant, client, ge_12730, integration) async def get_zwave_speed_from_percentage(percentage): """Set the fan to a particular percentage and get the resulting Zwave speed.""" client.async_send_command.reset_mock() + await hass.services.async_call( "fan", "turn_on", @@ -448,6 +450,7 @@ async def test_inovelli_lzw36( async def get_zwave_speed_from_percentage(percentage): """Set the fan to a particular percentage and get the resulting Zwave speed.""" client.async_send_command.reset_mock() + await hass.services.async_call( "fan", "turn_on", @@ -518,6 +521,7 @@ async def test_inovelli_lzw36( assert state.attributes[ATTR_PERCENTAGE] is None client.async_send_command.reset_mock() + await hass.services.async_call( "fan", "turn_on", @@ -553,6 +557,7 @@ async def test_leviton_zw4sf_fan( async def get_zwave_speed_from_percentage(percentage): """Set the fan to a particular percentage and get the resulting Zwave speed.""" client.async_send_command.reset_mock() + await hass.services.async_call( "fan", "turn_on", @@ -951,6 +956,7 @@ async def test_honeywell_39358_fan( async def get_zwave_speed_from_percentage(percentage): """Set the fan to a particular percentage and get the resulting Zwave speed.""" client.async_send_command.reset_mock() + await hass.services.async_call( "fan", "turn_on", diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index aaa2907d30a..e38873322ae 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -1,7 +1,10 @@ """Test the Z-Wave JS helpers module.""" +import voluptuous as vol + from homeassistant.components.zwave_js.helpers import ( async_get_node_status_sensor_entity_id, async_get_nodes_from_area_id, + get_value_state_schema, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import area_registry as ar, device_registry as dr @@ -22,3 +25,14 @@ async def test_async_get_nodes_from_area_id(hass: HomeAssistant) -> None: area_reg = ar.async_get(hass) area = area_reg.async_create("test") assert not async_get_nodes_from_area_id(hass, area.id) + + +async def test_get_value_state_schema_boolean_config_value( + hass: HomeAssistant, client, aeon_smart_switch_6 +) -> None: + """Test get_value_state_schema for boolean config value.""" + schema_validator = get_value_state_schema( + aeon_smart_switch_6.values["102-112-0-255"] + ) + assert isinstance(schema_validator, vol.Coerce) + assert schema_validator.type == bool diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3ec1f113b3e..c421e043413 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1005,7 +1005,7 @@ async def test_node_removed( event = { "source": "controller", "event": "node added", - "node": node.data, + "node": multisensor_6_state, "result": {}, } @@ -1014,7 +1014,7 @@ async def test_node_removed( old_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert old_device.id - event = {"node": node, "replaced": False} + event = {"node": node, "reason": 0} client.driver.controller.emit("node removed", event) await hass.async_block_till_done() @@ -1047,14 +1047,14 @@ async def test_replace_same_node( assert hass.states.get(AIR_TEMPERATURE_SENSOR) - # A replace node event has the extra field "replaced" set to True + # A replace node event has the extra field "reason" # to distinguish it from an exclusion event = Event( type="node removed", data={ "source": "controller", "event": "node removed", - "replaced": True, + "reason": 3, "node": multisensor_6_state, }, ) @@ -1139,8 +1139,8 @@ async def test_replace_different_node( """Test when a node is replaced with a different node.""" dev_reg = dr.async_get(hass) node_id = multisensor_6.node_id - hank_binary_switch_state = deepcopy(hank_binary_switch_state) - hank_binary_switch_state["nodeId"] = node_id + state = deepcopy(hank_binary_switch_state) + state["nodeId"] = node_id device_id = f"{client.driver.controller.home_id}-{node_id}" multisensor_6_device_id = ( @@ -1148,9 +1148,9 @@ async def test_replace_different_node( f"{multisensor_6.product_type}:{multisensor_6.product_id}" ) hank_device_id = ( - f"{device_id}-{hank_binary_switch_state['manufacturerId']}:" - f"{hank_binary_switch_state['productType']}:" - f"{hank_binary_switch_state['productId']}" + f"{device_id}-{state['manufacturerId']}:" + f"{state['productType']}:" + f"{state['productId']}" ) device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) @@ -1171,7 +1171,7 @@ async def test_replace_different_node( data={ "source": "controller", "event": "node removed", - "replaced": True, + "reason": 3, "node": multisensor_6_state, }, ) @@ -1228,7 +1228,7 @@ async def test_replace_different_node( "source": "node", "event": "ready", "nodeId": node_id, - "nodeState": hank_binary_switch_state, + "nodeState": state, }, ) client.driver.receive_event(event) @@ -1345,7 +1345,7 @@ async def test_disabled_node_status_entity_on_node_replaced( data={ "source": "controller", "event": "node removed", - "replaced": True, + "reason": 3, "node": zp3111_state, }, ) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 54638358fe7..ccbe956fbe5 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -414,6 +414,7 @@ async def test_bulk_set_config_parameters( identifiers={get_device_id(client.driver, multisensor_6)} ) assert device + # Test setting config parameter by property and property_key await hass.services.async_call( DOMAIN, @@ -875,7 +876,9 @@ async def test_set_value( client.async_send_command.reset_mock() # Test that when a command fails we raise an exception - client.async_send_command.return_value = {"success": False} + client.async_send_command.return_value = { + "result": {"status": 2, "message": "test"} + } with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -924,7 +927,6 @@ async def test_set_value_string( hass: HomeAssistant, client, climate_danfoss_lc_13, lock_schlage_be469, integration ) -> None: """Test set_value service converts number to string when needed.""" - client.async_send_command.return_value = {"success": True} # Test that number gets converted to a string when needed await hass.services.async_call( @@ -1240,7 +1242,9 @@ async def test_multicast_set_value( ) # Test that when a command is unsuccessful we raise an exception - client.async_send_command.return_value = {"success": False} + client.async_send_command.return_value = { + "result": {"status": 2, "message": "test"} + } with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -1381,7 +1385,7 @@ async def test_multicast_set_value_string( integration, ) -> None: """Test multicast_set_value service converts number to string when needed.""" - client.async_send_command.return_value = {"success": True} + client.async_send_command.return_value = {"result": {"status": 255}} # Test that number gets converted to a string when needed await hass.services.async_call( diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index ebf7d9f441f..fd5c626bdd2 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -63,6 +63,8 @@ async def test_switch( state = hass.states.get(SWITCH_ENTITY) assert state.state == "on" + client.async_send_command.reset_mock() + # Test turning off await hass.services.async_call( "switch", "turn_off", {"entity_id": SWITCH_ENTITY}, blocking=True diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 501ad13cbaa..25553489b4e 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -1158,7 +1158,7 @@ async def test_server_reconnect_event( data={ "source": "controller", "event": "node removed", - "replaced": False, + "reason": 0, "node": lock_schlage_be469_state, }, ) @@ -1238,7 +1238,7 @@ async def test_server_reconnect_value_updated( data={ "source": "controller", "event": "node removed", - "replaced": False, + "reason": 0, "node": lock_schlage_be469_state, }, ) diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index dcd71789e84..5234460bb51 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -264,6 +264,8 @@ async def test_update_entity_ha_not_running( """Test update occurs only after HA is running.""" await hass.async_stop() + client.async_send_command.return_value = {"updates": []} + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -341,7 +343,9 @@ async def test_update_entity_progress( assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = {"success": False} + client.async_send_command.return_value = { + "result": {"status": 2, "success": False, "reInterview": False} + } # Test successful install call without a version install_task = hass.async_create_task( @@ -437,7 +441,9 @@ async def test_update_entity_install_failed( assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = {"success": False} + client.async_send_command.return_value = { + "result": {"status": 2, "success": False, "reInterview": False} + } # Test install call - we expect it to finish fail install_task = hass.async_create_task( @@ -577,6 +583,7 @@ async def test_update_entity_delay( ) -> None: """Test update occurs on a delay after HA starts.""" client.async_send_command.reset_mock() + client.async_send_command.return_value = {"updates": []} await hass.async_stop() entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) @@ -710,7 +717,9 @@ async def test_update_entity_full_restore_data_update_available( assert state.attributes[ATTR_SKIPPED_VERSION] is None assert state.attributes[ATTR_LATEST_VERSION] == "11.2.4" - client.async_send_command.return_value = {"success": True} + client.async_send_command.return_value = { + "result": {"status": 255, "success": True, "reInterview": False} + } # Test successful install call without a version install_task = hass.async_create_task( From 056d00fb11061a675faf0ac5ac3a578d187b11ab Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 10 Aug 2023 02:08:14 -0400 Subject: [PATCH 092/179] Update zwave_js entity naming logic (#98140) * Update zwave_js entity naming logic * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare * store primary value locally --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/entity.py | 26 ++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 6cf2a402f3f..7017254034a 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -161,6 +161,7 @@ class ZWaveBaseEntity(Entity): name_prefix: str | None = None, ) -> str: """Generate entity name.""" + primary_value = self.info.primary_value name = "" if ( hasattr(self, "entity_description") @@ -178,9 +179,9 @@ class ZWaveBaseEntity(Entity): value_name = alternate_value_name elif include_value_name: value_name = ( - self.info.primary_value.metadata.label - or self.info.primary_value.property_key_name - or self.info.primary_value.property_name + primary_value.metadata.label + or primary_value.property_key_name + or primary_value.property_name or "" ) @@ -188,12 +189,21 @@ class ZWaveBaseEntity(Entity): # Only include non empty additional info if additional_info := [item for item in (additional_info or []) if item]: name = f"{name} {' '.join(additional_info)}" - # append endpoint if > 1 - if ( - self.info.primary_value.endpoint is not None - and self.info.primary_value.endpoint > 1 + + # Only append endpoint to name if there are equivalent values on a lower + # endpoint + if primary_value.endpoint is not None and any( + get_value_id_str( + self.info.node, + primary_value.command_class, + primary_value.property_, + endpoint=endpoint_idx, + property_key=primary_value.property_key, + ) + in self.info.node.values + for endpoint_idx in range(0, primary_value.endpoint) ): - name += f" ({self.info.primary_value.endpoint})" + name += f" ({primary_value.endpoint})" return name From e05b74668ca0fdcba31f038f371ccb4f74472881 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Aug 2023 20:31:57 -1000 Subject: [PATCH 093/179] Bump dbus-fast to 1.91.2 (#98105) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 147d38203aa..481a760ba88 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.7.0", - "dbus-fast==1.90.1" + "dbus-fast==1.91.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 61ed6ab3dcd..0f1fdcccf15 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.7.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.90.1 +dbus-fast==1.91.2 fnv-hash-fast==0.4.0 ha-av==10.1.1 hass-nabucasa==0.69.0 diff --git a/requirements_all.txt b/requirements_all.txt index be5cbbfdf6f..75b32f45250 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -632,7 +632,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.90.1 +dbus-fast==1.91.2 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eb00604a77..073de390e61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.90.1 +dbus-fast==1.91.2 # homeassistant.components.debugpy debugpy==1.6.7 From 3dd377cb2a0b60593a18767a5e4b032f5630fd78 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 10 Aug 2023 08:37:59 +0200 Subject: [PATCH 094/179] Update orjson to 3.9.4 (#98108) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0f1fdcccf15..f3cfab069f0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ janus==1.0.0 Jinja2==3.1.2 lru-dict==1.2.0 mutagen==1.46.0 -orjson==3.9.3 +orjson==3.9.4 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.0.0 diff --git a/pyproject.toml b/pyproject.toml index 3ee1bf33477..3cf26e71cb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "cryptography==41.0.3", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", - "orjson==3.9.3", + "orjson==3.9.4", "packaging>=23.1", "pip>=21.3.1", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index 9cca2393a0d..f3cd10a3577 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.2.0 PyJWT==2.8.0 cryptography==41.0.3 pyOpenSSL==23.2.0 -orjson==3.9.3 +orjson==3.9.4 packaging>=23.1 pip>=21.3.1 python-slugify==4.0.1 From 355ef4eac85c0db2eddca0e83739471782089774 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Thu, 10 Aug 2023 10:19:27 +0200 Subject: [PATCH 095/179] Add unique_id to frontier_silicon media_player entity (#97955) --- .../frontier_silicon/media_player.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 62df3a12c2b..9e4db6fc3ca 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -39,7 +39,16 @@ async def async_setup_entry( afsapi: AFSAPI = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([AFSAPIDevice(config_entry.title, afsapi)], True) + async_add_entities( + [ + AFSAPIDevice( + config_entry.unique_id or config_entry.entry_id, + config_entry.title, + afsapi, + ) + ], + True, + ) class AFSAPIDevice(MediaPlayerEntity): @@ -67,15 +76,15 @@ class AFSAPIDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - def __init__(self, name: str | None, afsapi: AFSAPI) -> None: + def __init__(self, unique_id: str, name: str | None, afsapi: AFSAPI) -> None: """Initialize the Frontier Silicon API device.""" self.fs_device = afsapi self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, afsapi.webfsapi_endpoint)}, + identifiers={(DOMAIN, unique_id)}, name=name, ) - + self._attr_unique_id = f"{unique_id}_media_player" self._max_volume: int | None = None self.__modes_by_label: dict[str, str] | None = None @@ -114,8 +123,6 @@ class AFSAPIDevice(MediaPlayerEntity): ) self._attr_available = True - if not self._attr_name: - self._attr_name = await afsapi.get_friendly_name() if not self._attr_source_list: self.__modes_by_label = { From 5dcffca88da38345bc25a275b982c9ebecf3c93c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 10:41:06 +0200 Subject: [PATCH 096/179] Move Rova constants to separate file (#97566) * Move Rova constants to separate file * Update homeassistant/components/rova/const.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/rova/const.py | 8 ++++++++ homeassistant/components/rova/sensor.py | 21 ++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/rova/const.py diff --git a/homeassistant/components/rova/const.py b/homeassistant/components/rova/const.py new file mode 100644 index 00000000000..71d39d3703b --- /dev/null +++ b/homeassistant/components/rova/const.py @@ -0,0 +1,8 @@ +"""Const file for Rova.""" +import logging + +LOGGER = logging.getLogger(__package__) + +CONF_ZIP_CODE = "zip_code" +CONF_HOUSE_NUMBER = "house_number" +CONF_HOUSE_NUMBER_SUFFIX = "house_number_suffix" diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index f68ffbd0eaf..c7ac8a9c676 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime, timedelta -import logging from requests.exceptions import ConnectTimeout, HTTPError from rova.rova import Rova @@ -22,10 +21,12 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone -# Config for rova requests. -CONF_ZIP_CODE = "zip_code" -CONF_HOUSE_NUMBER = "house_number" -CONF_HOUSE_NUMBER_SUFFIX = "house_number_suffix" +from .const import ( + CONF_HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX, + CONF_ZIP_CODE, + LOGGER, +) UPDATE_DELAY = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12) @@ -66,8 +67,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -_LOGGER = logging.getLogger(__name__) - def setup_platform( hass: HomeAssistant, @@ -87,10 +86,10 @@ def setup_platform( try: if not api.is_rova_area(): - _LOGGER.error("ROVA does not collect garbage in this area") + LOGGER.error("ROVA does not collect garbage in this area") return except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve details from ROVA API") + LOGGER.error("Could not retrieve details from ROVA API") return # Create rova data service which will retrieve and update the data. @@ -140,7 +139,7 @@ class RovaData: try: items = self.api.get_calendar_items() except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, retry again later") + LOGGER.error("Could not retrieve data, retry again later") return self.data = {} @@ -153,4 +152,4 @@ class RovaData: if code not in self.data: self.data[code] = date - _LOGGER.debug("Updated Rova calendar: %s", self.data) + LOGGER.debug("Updated Rova calendar: %s", self.data) From 84d779fab7aca6d314721c60d23b12ab57834461 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 10 Aug 2023 02:13:55 -0700 Subject: [PATCH 097/179] Bump opower to 0.0.26 (#98141) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 523cbe1d988..73942231b40 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.24"] + "requirements": ["opower==0.0.26"] } diff --git a/requirements_all.txt b/requirements_all.txt index 75b32f45250..9cd4277ee74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1365,7 +1365,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.24 +opower==0.0.26 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 073de390e61..f67fdd61e15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1034,7 +1034,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.24 +opower==0.0.26 # homeassistant.components.oralb oralb-ble==0.17.6 From 5812090eff4c9652e92dc5f15b650a18a52f1f09 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 10 Aug 2023 03:11:01 -0700 Subject: [PATCH 098/179] Get Opower accounts from the customer endpoint (#98144) Get accounts from the customer endpoint --- homeassistant/components/opower/coordinator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index c331f45bc49..b346df1211c 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -69,12 +69,12 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): raise ConfigEntryAuthFailed from err forecasts: list[Forecast] = await self.api.async_get_forecast() _LOGGER.debug("Updating sensor data with: %s", forecasts) - await self._insert_statistics([forecast.account for forecast in forecasts]) + await self._insert_statistics() return {forecast.account.utility_account_id: forecast for forecast in forecasts} - async def _insert_statistics(self, accounts: list[Account]) -> None: + async def _insert_statistics(self) -> None: """Insert Opower statistics.""" - for account in accounts: + for account in await self.api.async_get_accounts(): id_prefix = "_".join( ( self.api.utility.subdomain(), From b872d74b1f1fdd51303d4094aea140978f93eeaa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 10 Aug 2023 12:16:52 +0200 Subject: [PATCH 099/179] Fix lingering test alexa (#98128) --- tests/components/alexa/test_smart_home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 0080c9b02b8..708b06bab2b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4285,7 +4285,7 @@ async def test_initialize_camera_stream( msg = await smart_home.async_handle_message( hass, get_default_config(hass), request ) - await hass.async_block_till_done() + await hass.async_stop() assert "event" in msg response = msg["event"] From 9b743214874e5aa4a48c5a2095f1597165fe53a4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 10 Aug 2023 12:37:28 +0200 Subject: [PATCH 100/179] Correct unit of rain pause (#98131) --- homeassistant/components/gardena_bluetooth/number.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index c425d17621d..ec887458586 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -62,11 +62,11 @@ DESCRIPTIONS = ( GardenaBluetoothNumberEntityDescription( key=DeviceConfiguration.rain_pause.uuid, translation_key="rain_pause", - native_unit_of_measurement=UnitOfTime.DAYS, + native_unit_of_measurement=UnitOfTime.MINUTES, mode=NumberMode.BOX, native_min_value=0.0, - native_max_value=127.0, - native_step=1.0, + native_max_value=7 * 24 * 60, + native_step=6 * 60.0, entity_category=EntityCategory.CONFIG, char=DeviceConfiguration.rain_pause, ), From 4531dbbe62885399998c712b78dcf6a973d4ef13 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 10 Aug 2023 12:59:23 +0200 Subject: [PATCH 101/179] Refactor Rest Binary sensor with ManualTriggerEntity (#97400) * Refactor Rest Binary sensor w/ ManualTriggerEntity * test availability * review comments * Use super * Fix config --- .../components/rest/binary_sensor.py | 51 ++++++++++++++----- homeassistant/components/rest/schema.py | 2 + homeassistant/helpers/template_entity.py | 1 + tests/components/rest/test_binary_sensor.py | 24 +++++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 0c1f4df6093..7ab632995ea 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -14,6 +14,8 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, + CONF_ICON, + CONF_NAME, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, CONF_UNIQUE_ID, @@ -24,7 +26,11 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import TemplateEntity +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, + ManualTriggerEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -42,6 +48,14 @@ PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA ) +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, +) + async def async_setup_platform( hass: HomeAssistant, @@ -74,7 +88,14 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - unique_id = conf.get(CONF_UNIQUE_ID) + name = conf.get(CONF_NAME) or Template(DEFAULT_BINARY_SENSOR_NAME, hass) + + trigger_entity_config = {CONF_NAME: name} + + for key in TRIGGER_ENTITY_OPTIONS: + if key not in conf: + continue + trigger_entity_config[key] = conf[key] async_add_entities( [ @@ -83,13 +104,13 @@ async def async_setup_platform( coordinator, rest, conf, - unique_id, + trigger_entity_config, ) ], ) -class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): +class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity): """Representation of a REST binary sensor.""" def __init__( @@ -98,9 +119,10 @@ class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): coordinator: DataUpdateCoordinator[None] | None, rest: RestData, config: ConfigType, - unique_id: str | None, + trigger_entity_config: ConfigType, ) -> None: """Initialize a REST binary sensor.""" + ManualTriggerEntity.__init__(self, hass, trigger_entity_config) RestEntity.__init__( self, coordinator, @@ -108,19 +130,17 @@ class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): config.get(CONF_RESOURCE_TEMPLATE), config[CONF_FORCE_UPDATE], ) - TemplateEntity.__init__( - self, - hass, - config=config, - fallback_name=DEFAULT_BINARY_SENSOR_NAME, - unique_id=unique_id, - ) self._previous_data = None self._value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) if (value_template := self._value_template) is not None: value_template.hass = hass - self._attr_device_class = config.get(CONF_DEVICE_CLASS) + @property + def available(self) -> bool: + """Return if entity is available.""" + available1 = RestEntity.available.fget(self) # type: ignore[attr-defined] + available2 = ManualTriggerEntity.available.fget(self) # type: ignore[attr-defined] + return bool(available1 and available2) def _update_from_rest_data(self) -> None: """Update state from the rest data.""" @@ -130,6 +150,8 @@ class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): response = self.rest.data + raw_value = response + if self._value_template is not None: response = self._value_template.async_render_with_possible_json_value( self.rest.data, False @@ -144,3 +166,6 @@ class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): "open": True, "yes": True, }.get(response.lower(), False) + + self._process_manual_data(raw_value) + self.async_write_ha_state() diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index c5abe42d7fc..c1f51286673 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -28,6 +28,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, TEMPLATE_ENTITY_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA, ) @@ -82,6 +83,7 @@ BINARY_SENSOR_SCHEMA = { vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_AVAILABILITY): cv.template, } diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 2e5cebf8571..07dd154922c 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -564,6 +564,7 @@ class TriggerBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" + await super().async_added_to_hass() template_attach(self.hass, self._config) def _set_unique_id(self, unique_id: str | None) -> None: diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 86bac75de91..896f5544d93 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -500,3 +500,27 @@ async def test_entity_config(hass: HomeAssistant) -> None: "friendly_name": "REST Binary Sensor", "icon": "mdi:one_two_three", } + + +@respx.mock +async def test_availability_in_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + BINARY_SENSOR_DOMAIN: { + # REST configuration + "platform": DOMAIN, + "method": "GET", + "resource": "http://localhost", + # Entity configuration + "availability": "{{value==1}}", + "name": "{{'REST' + ' ' + 'Binary Sensor'}}", + }, + } + + respx.get("http://localhost") % HTTPStatus.OK + assert await async_setup_component(hass, BINARY_SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.rest_binary_sensor") + assert state.state == STATE_UNAVAILABLE From e9f9c7799a5b81be8ce95fcb63bdb969a5bdc3be Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 13:05:58 +0200 Subject: [PATCH 102/179] Add device to cert expiry (#98152) * Add device to cert expiry * Apply suggestions from code review Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/cert_expiry/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 56bcf07a3bb..306ac7f9e3d 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -14,6 +14,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -99,6 +101,11 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): super().__init__(coordinator) self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})" self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{coordinator.host}:{coordinator.port}")}, + name=coordinator.name, + entry_type=DeviceEntryType.SERVICE, + ) @property def native_value(self) -> datetime | None: From 726b0c51795a6e353b56f22fca8ceac88001eb97 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Thu, 10 Aug 2023 13:58:48 +0200 Subject: [PATCH 103/179] Address late comments in #97955 (#98165) --- homeassistant/components/frontier_silicon/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 9e4db6fc3ca..490cc89febc 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -42,7 +42,7 @@ async def async_setup_entry( async_add_entities( [ AFSAPIDevice( - config_entry.unique_id or config_entry.entry_id, + config_entry.entry_id, config_entry.title, afsapi, ) @@ -84,7 +84,7 @@ class AFSAPIDevice(MediaPlayerEntity): identifiers={(DOMAIN, unique_id)}, name=name, ) - self._attr_unique_id = f"{unique_id}_media_player" + self._attr_unique_id = unique_id self._max_volume: int | None = None self.__modes_by_label: dict[str, str] | None = None From 868a5f377f98f82811c3d4d7447f0baa44729681 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Aug 2023 14:27:03 +0200 Subject: [PATCH 104/179] Ruff: isort don't split imports based on trailing comma (#98162) --- .../components/analytics/analytics.py | 4 +--- .../components/arcam_fmj/device_trigger.py | 4 +--- homeassistant/components/august/activity.py | 5 +---- .../components/aurora/coordinator.py | 5 +---- homeassistant/components/aurora/entity.py | 9 ++------- homeassistant/components/awair/__init__.py | 6 +----- .../bluetooth/passive_update_processor.py | 10 ++-------- .../bluetooth/update_coordinator.py | 10 ++-------- homeassistant/components/bthome/logbook.py | 6 +----- .../components/cert_expiry/__init__.py | 6 +----- homeassistant/components/duotecno/cover.py | 5 +---- homeassistant/components/duotecno/light.py | 6 +----- .../components/enphase_envoy/config_flow.py | 6 +----- .../components/enphase_envoy/const.py | 5 +---- .../components/enphase_envoy/coordinator.py | 6 +----- homeassistant/components/esphome/__init__.py | 9 ++------- .../components/esphome/alarm_control_panel.py | 6 +----- .../components/esphome/binary_sensor.py | 6 +----- .../components/esphome/bluetooth/__init__.py | 5 +---- homeassistant/components/esphome/button.py | 5 +---- homeassistant/components/esphome/camera.py | 5 +---- homeassistant/components/esphome/climate.py | 6 +----- homeassistant/components/esphome/cover.py | 6 +----- homeassistant/components/esphome/entity.py | 15 +++------------ homeassistant/components/esphome/fan.py | 6 +----- homeassistant/components/esphome/light.py | 6 +----- homeassistant/components/esphome/lock.py | 6 +----- homeassistant/components/esphome/manager.py | 6 +----- .../components/esphome/media_player.py | 6 +----- homeassistant/components/esphome/number.py | 6 +----- homeassistant/components/esphome/sensor.py | 6 +----- homeassistant/components/esphome/switch.py | 6 +----- .../components/ezviz/alarm_control_panel.py | 6 +----- homeassistant/components/ezviz/image.py | 13 +++---------- homeassistant/components/fivem/__init__.py | 4 +--- homeassistant/components/fivem/coordinator.py | 5 +---- homeassistant/components/fivem/entity.py | 9 ++------- .../components/freebox/binary_sensor.py | 4 +--- .../components/gardena_bluetooth/button.py | 5 +---- .../components/geo_location/trigger.py | 8 +------- .../homeassistant/triggers/state.py | 8 +------- homeassistant/components/hue/event.py | 5 +---- homeassistant/components/knx/light.py | 6 +----- .../components/lastfm/coordinator.py | 6 +----- homeassistant/components/lastfm/sensor.py | 4 +--- homeassistant/components/mqtt/event.py | 12 ++---------- homeassistant/components/mqtt/image.py | 5 +---- homeassistant/components/mqtt/scene.py | 6 +----- homeassistant/components/nobo_hub/climate.py | 6 +----- homeassistant/components/nobo_hub/sensor.py | 6 +----- .../components/opensky/config_flow.py | 7 +------ .../components/pegel_online/__init__.py | 5 +---- homeassistant/components/rova/sensor.py | 7 +------ homeassistant/components/smhi/weather.py | 6 +----- homeassistant/components/template/image.py | 10 ++-------- homeassistant/components/unifi/switch.py | 4 +--- .../components/utility_meter/select.py | 5 +---- homeassistant/components/voip/voip.py | 8 +------- homeassistant/components/wemo/fan.py | 5 +---- homeassistant/components/zone/trigger.py | 7 +------ homeassistant/core.py | 11 +---------- homeassistant/helpers/entity_platform.py | 5 +---- pyproject.toml | 1 + .../advantage_air/test_diagnostics.py | 6 +----- .../components/climate/test_device_action.py | 4 +--- tests/components/datadog/test_init.py | 6 +----- .../components/deconz/test_device_trigger.py | 5 +---- .../devolo_home_network/test_button.py | 5 +---- tests/components/esphome/conftest.py | 4 +--- .../esphome/test_alarm_control_panel.py | 6 +----- tests/components/esphome/test_button.py | 5 +---- tests/components/esphome/test_camera.py | 4 +--- tests/components/esphome/test_config_flow.py | 5 +---- tests/components/esphome/test_diagnostics.py | 5 +---- tests/components/esphome/test_manager.py | 7 +------ tests/components/esphome/test_media_player.py | 4 +--- tests/components/esphome/test_sensor.py | 6 +----- tests/components/esphome/test_update.py | 18 +++--------------- tests/components/fritz/test_update.py | 6 +----- .../components/gardena_bluetooth/__init__.py | 4 +--- .../gardena_bluetooth/test_button.py | 5 +---- .../gardena_bluetooth/test_config_flow.py | 4 +--- .../gardena_bluetooth/test_number.py | 5 +---- .../homekit_controller/test_device_trigger.py | 5 +---- .../components/hue/test_device_trigger_v2.py | 5 +---- tests/components/hue/test_event.py | 5 +---- tests/components/influxdb/test_init.py | 7 +------ tests/components/lastfm/test_sensor.py | 5 +---- tests/components/light/test_device_action.py | 5 +---- tests/components/matter/test_door_lock.py | 5 +---- tests/components/matter/test_event.py | 10 ++-------- tests/components/mqtt/test_common.py | 5 +---- tests/components/mqtt/test_event.py | 9 ++------- tests/components/mqtt/test_mixins.py | 5 +---- tests/components/nest/test_device_trigger.py | 5 +---- tests/components/opensky/test_config_flow.py | 6 +----- tests/components/oralb/test_sensor.py | 5 +---- .../pegel_online/test_config_flow.py | 5 +---- .../philips_js/test_device_trigger.py | 5 +---- .../components/qingping/test_binary_sensor.py | 6 +----- tests/components/rfxtrx/test_device_action.py | 5 +---- .../components/shelly/test_device_trigger.py | 5 +---- tests/components/smhi/test_weather.py | 5 +---- .../components/subaru/test_device_tracker.py | 6 +----- .../components/tasmota/test_device_trigger.py | 5 +---- tests/components/template/test_image.py | 6 +----- tests/components/transport_nsw/test_sensor.py | 5 +---- tests/components/unifi/test_button.py | 19 ++++--------------- tests/components/unifi/test_image.py | 4 +--- tests/components/weather/test_recorder.py | 5 +---- tests/components/websocket_api/conftest.py | 5 +---- .../components/websocket_api/test_commands.py | 5 +---- tests/components/websocket_api/test_http.py | 5 +---- tests/components/wemo/test_device_trigger.py | 5 +---- tests/components/zha/test_device_action.py | 6 +----- .../zwave_js/test_device_trigger.py | 5 +---- tests/util/test_distance.py | 4 +--- 117 files changed, 135 insertions(+), 580 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 19e6b5ec7b3..a106e3f0068 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -22,9 +22,7 @@ from homeassistant.components.recorder import ( get_instance as get_recorder_instance, ) import homeassistant.config as conf_util -from homeassistant.config_entries import ( - SOURCE_IGNORE, -) +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index ef83217ee26..174ffda9622 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -3,9 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 3909e36ded8..1768d3291a7 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -4,10 +4,7 @@ from datetime import datetime import logging from aiohttp import ClientError -from yalexs.activity import ( - Activity, - ActivityType, -) +from yalexs.activity import Activity, ActivityType from yalexs.api_async import ApiAsync from yalexs.pubnub_async import AugustPubNub from yalexs.util import get_latest_activity diff --git a/homeassistant/components/aurora/coordinator.py b/homeassistant/components/aurora/coordinator.py index c126e2a8c68..973e48850a6 100644 --- a/homeassistant/components/aurora/coordinator.py +++ b/homeassistant/components/aurora/coordinator.py @@ -7,10 +7,7 @@ from aiohttp import ClientError from auroranoaa import AuroraForecast from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aurora/entity.py b/homeassistant/components/aurora/entity.py index 8948ff9c43c..88ae67daa9e 100644 --- a/homeassistant/components/aurora/entity.py +++ b/homeassistant/components/aurora/entity.py @@ -4,14 +4,9 @@ import logging from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - ATTRIBUTION, - DOMAIN, -) +from .const import ATTRIBUTION, DOMAIN from .coordinator import AuroraDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index dca885ffe0d..bfd95fece2a 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -13,11 +13,7 @@ from python_awair.devices import AwairBaseDevice, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - Platform, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 78965ae5cde..fa4d76b0cab 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -16,14 +16,8 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers.entity import ( - DeviceInfo, - Entity, - EntityDescription, -) -from homeassistant.helpers.entity_platform import ( - async_get_current_platform, -) +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity_platform import async_get_current_platform from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.storage import Store from homeassistant.util.enum import try_parse_enum diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 71729a1dba8..88263aa0a58 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -12,14 +12,8 @@ from .api import ( async_register_callback, async_track_unavailable, ) -from .match import ( - BluetoothCallbackMatcher, -) -from .models import ( - BluetoothChange, - BluetoothScanningMode, - BluetoothServiceInfoBleak, -) +from .match import BluetoothCallbackMatcher +from .models import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak class BasePassiveBluetoothCoordinator(ABC): diff --git a/homeassistant/components/bthome/logbook.py b/homeassistant/components/bthome/logbook.py index 4111777375d..158253ec8a7 100644 --- a/homeassistant/components/bthome/logbook.py +++ b/homeassistant/components/bthome/logbook.py @@ -8,11 +8,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import async_get from homeassistant.helpers.typing import EventType -from .const import ( - BTHOME_BLE_EVENT, - DOMAIN, - BTHomeBleEvent, -) +from .const import BTHOME_BLE_EVENT, DOMAIN, BTHomeBleEvent @callback diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 267a2d56236..5d1e68a951f 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -5,11 +5,7 @@ from datetime import datetime, timedelta import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - Platform, -) +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.start import async_at_started from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py index 0fd212df085..a6fb49c30e0 100644 --- a/homeassistant/components/duotecno/cover.py +++ b/homeassistant/components/duotecno/cover.py @@ -5,10 +5,7 @@ from typing import Any from duotecno.unit import DuoswitchUnit -from homeassistant.components.cover import ( - CoverEntity, - CoverEntityFeature, -) +from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/duotecno/light.py b/homeassistant/components/duotecno/light.py index 01d3bf488f1..da288b6cbe0 100644 --- a/homeassistant/components/duotecno/light.py +++ b/homeassistant/components/duotecno/light.py @@ -3,11 +3,7 @@ from typing import Any from duotecno.unit import DimUnit -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ColorMode, - LightEntity, -) +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 3ec39739ed7..b41d29626e7 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -6,11 +6,7 @@ import logging from typing import Any from awesomeversion import AwesomeVersion -from pyenphase import ( - AUTH_TOKEN_MIN_VERSION, - Envoy, - EnvoyError, -) +from pyenphase import AUTH_TOKEN_MIN_VERSION, Envoy, EnvoyError import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 662662aa8be..d10cc0b9511 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,8 +1,5 @@ """The enphase_envoy component.""" -from pyenphase import ( - EnvoyAuthenticationError, - EnvoyAuthenticationRequired, -) +from pyenphase import EnvoyAuthenticationError, EnvoyAuthenticationRequired from homeassistant.const import Platform diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index de1246fffa5..75f2ef39289 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -7,11 +7,7 @@ from datetime import timedelta import logging from typing import Any -from pyenphase import ( - Envoy, - EnvoyError, - EnvoyTokenAuth, -) +from pyenphase import Envoy, EnvoyError, EnvoyTokenAuth from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 4a36535cc9b..bc22cc13d6f 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,9 +1,7 @@ """Support for esphome devices.""" from __future__ import annotations -from aioesphomeapi import ( - APIClient, -) +from aioesphomeapi import APIClient from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry @@ -17,10 +15,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_NOISE_PSK, - DOMAIN, -) +from .const import CONF_NOISE_PSK, DOMAIN from .dashboard import async_setup as async_setup_dashboard from .domain_data import DomainData diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 639f47272d9..6f3f903f248 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -31,11 +31,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[ diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 65a237de4f7..4eb29f0c210 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -14,11 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum from .domain_data import DomainData -from .entity import ( - EsphomeAssistEntity, - EsphomeEntity, - platform_async_setup_entry, -) +from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry async def async_setup_entry( diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 4acd335c1b8..9ef298145d3 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -16,10 +16,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca from ..entry_data import RuntimeEntryData from .cache import ESPHomeBluetoothCache -from .client import ( - ESPHomeClient, - ESPHomeClientData, -) +from .client import ESPHomeClient, ESPHomeClientData from .device import ESPHomeBluetoothDevice from .scanner import ESPHomeScanner diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index eca8d226c69..a55acf067f0 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -9,10 +9,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import ( - EsphomeEntity, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, platform_async_setup_entry async def async_setup_entry( diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index f3fb8b867d8..98a4c26621d 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -15,10 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import ( - EsphomeEntity, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, platform_async_setup_entry async def async_setup_entry( diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index a9b184cc936..b34714ff89c 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -55,11 +55,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper FAN_QUIET = "quiet" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 45ef8a132f9..4dee3958515 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -17,11 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index c35b4dc9b13..fceb2778734 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -4,12 +4,7 @@ from __future__ import annotations from collections.abc import Callable import functools import math -from typing import ( # pylint: disable=unused-import - Any, - Generic, - TypeVar, - cast, -) +from typing import Any, Generic, TypeVar, cast # pylint: disable=unused-import from aioesphomeapi import ( EntityCategory as EsphomeEntityCategory, @@ -19,16 +14,12 @@ from aioesphomeapi import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - EntityCategory, -) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform 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.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 27a259f4441..a6ca52d6c1a 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -22,11 +22,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 1ecc99730bf..95fe864eea8 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -31,11 +31,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 00b94cd15ff..6a0d100e679 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -11,11 +11,7 @@ from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 71dc02acf02..a0f49340c1a 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -23,11 +23,7 @@ import voluptuous as vol from homeassistant.components import tag, zeroconf from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONF_MODE, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import ATTR_DEVICE_ID, CONF_MODE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 9d008300966..c77625b14dd 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -25,11 +25,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 6be1822f90f..4f3109f5a83 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -16,11 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 2e658389e03..af873565fc3 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -25,11 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from homeassistant.util.enum import try_parse_enum -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 99894b8501e..b2ceaf0fced 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import ( - EsphomeEntity, - esphome_state_property, - platform_async_setup_entry, -) +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py index 32f9b38888f..906739da4b3 100644 --- a/homeassistant/components/ezviz/alarm_control_panel.py +++ b/homeassistant/components/ezviz/alarm_control_panel.py @@ -24,11 +24,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - DATA_COORDINATOR, - DOMAIN, - MANUFACTURER, -) +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER from .coordinator import EzvizDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py index 9bc65f12355..3de4f55a9d4 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -4,19 +4,12 @@ from __future__ import annotations import logging from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription -from homeassistant.config_entries import ( - ConfigEntry, -) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import ( - AddEntitiesCallback, -) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import ( - DATA_COORDINATOR, - DOMAIN, -) +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator from .entity import EzvizEntity diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 93adda2b4fd..996aecef261 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -10,9 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import ( - DOMAIN, -) +from .const import DOMAIN from .coordinator import FiveMDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] diff --git a/homeassistant/components/fivem/coordinator.py b/homeassistant/components/fivem/coordinator.py index e7fa4c426db..9da641b0bd9 100644 --- a/homeassistant/components/fivem/coordinator.py +++ b/homeassistant/components/fivem/coordinator.py @@ -10,10 +10,7 @@ from fivem import FiveM, FiveMServerOfflineError from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_PLAYERS_LIST, diff --git a/homeassistant/components/fivem/entity.py b/homeassistant/components/fivem/entity.py index 53c35716276..cfd9d502b2f 100644 --- a/homeassistant/components/fivem/entity.py +++ b/homeassistant/components/fivem/entity.py @@ -7,14 +7,9 @@ import logging from typing import Any from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - MANUFACTURER, -) +from .const import DOMAIN, MANUFACTURER from .coordinator import FiveMDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/freebox/binary_sensor.py b/homeassistant/components/freebox/binary_sensor.py index aabd07366b4..10a151dbcf6 100644 --- a/homeassistant/components/freebox/binary_sensor.py +++ b/homeassistant/components/freebox/binary_sensor.py @@ -10,9 +10,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - EntityCategory, -) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py index b984d3420ae..a9dac9902f8 100644 --- a/homeassistant/components/gardena_bluetooth/button.py +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -6,10 +6,7 @@ from dataclasses import dataclass, field from gardena_bluetooth.const import Reset from gardena_bluetooth.parse import CharacteristicBool -from homeassistant.components.button import ( - ButtonEntity, - ButtonEntityDescription, -) +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 5527f5ec9f1..f4ed94f1cf4 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -7,13 +7,7 @@ from typing import Final import voluptuous as vol from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE -from homeassistant.core import ( - CALLBACK_TYPE, - HassJob, - HomeAssistant, - State, - callback, -) +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import ( diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index eec66a560a5..2cac07e7cd9 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -8,13 +8,7 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL -from homeassistant.core import ( - CALLBACK_TYPE, - HassJob, - HomeAssistant, - State, - callback, -) +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback from homeassistant.helpers import ( config_validation as cv, entity_registry as er, diff --git a/homeassistant/components/hue/event.py b/homeassistant/components/hue/event.py index 8e34f7a22bf..914067509b7 100644 --- a/homeassistant/components/hue/event.py +++ b/homeassistant/components/hue/event.py @@ -6,10 +6,7 @@ from typing import Any from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.models.button import Button -from aiohue.v2.models.relative_rotary import ( - RelativeRotary, - RelativeRotaryDirection, -) +from aiohue.v2.models.relative_rotary import RelativeRotary, RelativeRotaryDirection from homeassistant.components.event import ( EventDeviceClass, diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 07747f094c3..f25e78a4d70 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -4,11 +4,7 @@ from __future__ import annotations from typing import Any, cast from xknx import XKNX -from xknx.devices.light import ( - ColorTemperatureType, - Light as XknxLight, - XYYColor, -) +from xknx.devices.light import ColorTemperatureType, Light as XknxLight, XYYColor from homeassistant import config_entries from homeassistant.components.light import ( diff --git a/homeassistant/components/lastfm/coordinator.py b/homeassistant/components/lastfm/coordinator.py index 533f9ec3b09..6e62fe2c84e 100644 --- a/homeassistant/components/lastfm/coordinator.py +++ b/homeassistant/components/lastfm/coordinator.py @@ -11,11 +11,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CONF_USERS, - DOMAIN, - LOGGER, -) +from .const import CONF_USERS, DOMAIN, LOGGER def format_track(track: Track | None) -> str | None: diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 116a0813387..0b2039436f4 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -16,9 +16,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_LAST_PLAYED, diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py index 5a94ec754c0..6f8be33f21a 100644 --- a/homeassistant/components/mqtt/event.py +++ b/homeassistant/components/mqtt/event.py @@ -15,11 +15,7 @@ from homeassistant.components.event import ( EventEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_DEVICE_CLASS, - CONF_NAME, - CONF_VALUE_TEMPLATE, -) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,11 +32,7 @@ from .const import ( PAYLOAD_NONE, ) from .debug_info import log_messages -from .mixins import ( - MQTT_ENTITY_COMMON_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from .models import ( MqttValueTemplate, PayloadSentinel, diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index a21d45369f8..da62416d29e 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -12,10 +12,7 @@ import httpx import voluptuous as vol from homeassistant.components import image -from homeassistant.components.image import ( - DEFAULT_CONTENT_TYPE, - ImageEntity, -) +from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 87c56869d0c..fd876976fe6 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -17,11 +17,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .config import MQTT_BASE_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN -from .mixins import ( - MQTT_ENTITY_COMMON_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from .util import valid_publish_topic DEFAULT_NAME = "MQTT Scene" diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 00667c43fdb..b22206734f8 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -17,11 +17,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_NAME, - PRECISION_TENTHS, - UnitOfTemperature, -) +from homeassistant.const import ATTR_NAME, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 9cc957ec1df..3313aaf4ce7 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -9,11 +9,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_MODEL, - ATTR_NAME, - UnitOfTemperature, -) +from homeassistant.const import ATTR_MODEL, ATTR_NAME, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py index 6e3ffb5e2b1..12827dfd6ba 100644 --- a/homeassistant/components/opensky/config_flow.py +++ b/homeassistant/components/opensky/config_flow.py @@ -6,12 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/pegel_online/__init__.py b/homeassistant/components/pegel_online/__init__.py index a2767cb749b..e9e0e9d6aae 100644 --- a/homeassistant/components/pegel_online/__init__.py +++ b/homeassistant/components/pegel_online/__init__.py @@ -10,10 +10,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( - CONF_STATION, - DOMAIN, -) +from .const import CONF_STATION, DOMAIN from .coordinator import PegelOnlineDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index c7ac8a9c676..3565b3baf0d 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -21,12 +21,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone -from .const import ( - CONF_HOUSE_NUMBER, - CONF_HOUSE_NUMBER_SUFFIX, - CONF_ZIP_CODE, - LOGGER, -) +from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, LOGGER UPDATE_DELAY = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 6f50f5c9a65..2945f890df2 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -62,11 +62,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.util import Throttle, slugify -from .const import ( - ATTR_SMHI_THUNDER_PROBABILITY, - DOMAIN, - ENTITY_ID_SENSOR_FORMAT, -) +from .const import ATTR_SMHI_THUNDER_PROBABILITY, DOMAIN, ENTITY_ID_SENSOR_FORMAT _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/template/image.py b/homeassistant/components/template/image.py index 95bbac576ad..da0fbd68bc0 100644 --- a/homeassistant/components/template/image.py +++ b/homeassistant/components/template/image.py @@ -6,10 +6,7 @@ from typing import Any import voluptuous as vol -from homeassistant.components.image import ( - DOMAIN as IMAGE_DOMAIN, - ImageEntity, -) +from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN, ImageEntity from homeassistant.const import CONF_UNIQUE_ID, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError @@ -20,10 +17,7 @@ from homeassistant.util import dt as dt_util from . import TriggerUpdateCoordinator from .const import CONF_PICTURE -from .template_entity import ( - TemplateEntity, - make_template_entity_common_schema, -) +from .template_entity import TemplateEntity, make_template_entity_common_schema from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 140da492a96..f931bd06e1c 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -41,9 +41,7 @@ from homeassistant.components.switch import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import ( - DeviceEntryType, -) +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index cf2a6da9e08..01e554b1666 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -7,10 +7,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 3d0681a8475..ca78b604169 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -37,13 +37,7 @@ from homeassistant.const import __version__ from homeassistant.core import Context, HomeAssistant from homeassistant.util.ulid import ulid -from .const import ( - CHANNELS, - DOMAIN, - RATE, - RTP_AUDIO_SETTINGS, - WIDTH, -) +from .const import CHANNELS, DOMAIN, RATE, RTP_AUDIO_SETTINGS, WIDTH if TYPE_CHECKING: from .devices import VoIPDevice, VoIPDevices diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index aaa85455c56..e1c8655c196 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -20,10 +20,7 @@ from homeassistant.util.percentage import ( ) from . import async_wemo_dispatcher_connect -from .const import ( - SERVICE_RESET_FILTER_LIFE, - SERVICE_SET_HUMIDITY, -) +from .const import SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY from .entity import WemoBinaryStateEntity from .wemo_device import DeviceCoordinator diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 9412c612ca2..09f93f9b786 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -12,12 +12,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_ZONE, ) -from homeassistant.core import ( - CALLBACK_TYPE, - HassJob, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import ( condition, config_validation as cv, diff --git a/homeassistant/core.py b/homeassistant/core.py index 3673f9acba5..3b54358dc3d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -26,16 +26,7 @@ import re import threading import time from time import monotonic -from typing import ( - TYPE_CHECKING, - Any, - Generic, - ParamSpec, - Self, - TypeVar, - cast, - overload, -) +from typing import TYPE_CHECKING, Any, Generic, ParamSpec, Self, TypeVar, cast, overload from urllib.parse import urlparse import async_timeout diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index b7dadcf0f67..9d6a1d0e1d2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -26,10 +26,7 @@ from homeassistant.core import ( split_entity_id, valid_entity_id, ) -from homeassistant.exceptions import ( - HomeAssistantError, - PlatformNotReady, -) +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.generated import languages from homeassistant.setup import async_start_setup from homeassistant.util.async_ import run_callback_threadsafe diff --git a/pyproject.toml b/pyproject.toml index 3cf26e71cb2..02dbc87fb72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -537,6 +537,7 @@ known-first-party = [ "homeassistant", ] combine-as-imports = true +split-on-trailing-comma = false [tool.ruff.per-file-ignores] diff --git a/tests/components/advantage_air/test_diagnostics.py b/tests/components/advantage_air/test_diagnostics.py index ebd026c6cc7..01f6d809a49 100644 --- a/tests/components/advantage_air/test_diagnostics.py +++ b/tests/components/advantage_air/test_diagnostics.py @@ -3,11 +3,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant -from . import ( - TEST_SYSTEM_DATA, - TEST_SYSTEM_URL, - add_mock_config, -) +from . import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, add_mock_config from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 05c5c4cdebb..f56f499c935 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -5,9 +5,7 @@ import voluptuous_serialize import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, HVACMode, const, device_action -from homeassistant.components.device_automation import ( - DeviceAutomationType, -) +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import ( diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 76956874e73..1ae795f2e95 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -3,11 +3,7 @@ from unittest import mock from unittest.mock import patch import homeassistant.components.datadog as datadog -from homeassistant.const import ( - EVENT_LOGBOOK_ENTRY, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import EVENT_LOGBOOK_ENTRY, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index fe2ba8d4177..fe9d57f8a65 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -33,10 +33,7 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.common import ( - async_get_device_automations, - async_mock_service, -) +from tests.common import async_get_device_automations, async_mock_service from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/devolo_home_network/test_button.py b/tests/components/devolo_home_network/test_button.py index 4b8521b5798..41820210dee 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -5,10 +5,7 @@ from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnav import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.button import ( - DOMAIN as PLATFORM, - SERVICE_PRESS, -) +from homeassistant.components.button import DOMAIN as PLATFORM, SERVICE_PRESS from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ATTR_ENTITY_ID diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f373c2fdb17..4deae7f13fa 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -19,9 +19,7 @@ import async_timeout import pytest from zeroconf import Zeroconf -from homeassistant.components.esphome import ( - dashboard, -) +from homeassistant.components.esphome import dashboard from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, CONF_DEVICE_NAME, diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index 5a99f403394..e7409bdfae4 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -21,11 +21,7 @@ from homeassistant.components.alarm_control_panel import ( SERVICE_ALARM_TRIGGER, ) from homeassistant.components.esphome.alarm_control_panel import EspHomeACPFeatures -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_ALARM_ARMED_AWAY, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/esphome/test_button.py b/tests/components/esphome/test_button.py index f33026800e7..71406341175 100644 --- a/tests/components/esphome/test_button.py +++ b/tests/components/esphome/test_button.py @@ -5,10 +5,7 @@ from unittest.mock import call from aioesphomeapi import APIClient, ButtonInfo -from homeassistant.components.button import ( - DOMAIN as BUTTON_DOMAIN, - SERVICE_PRESS, -) +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py index f9a25d6b5f2..bbf51c3bc12 100644 --- a/tests/components/esphome/test_camera.py +++ b/tests/components/esphome/test_camera.py @@ -10,9 +10,7 @@ from aioesphomeapi import ( UserService, ) -from homeassistant.components.camera import ( - STATE_IDLE, -) +from homeassistant.components.camera import STATE_IDLE from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index fc37e1e51ee..63e18107623 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -17,10 +17,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf -from homeassistant.components.esphome import ( - DomainData, - dashboard, -) +from homeassistant.components.esphome import DomainData, dashboard from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, CONF_DEVICE_NAME, diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index a77fd9b0087..025c5bcaae8 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -1,10 +1,7 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" -from homeassistant.components.esphome.const import ( - CONF_DEVICE_NAME, - CONF_NOISE_PSK, -) +from homeassistant.components.esphome.const import CONF_DEVICE_NAME, CONF_NOISE_PSK from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 7a487f3a385..3bb298024f9 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1,12 +1,7 @@ """Test ESPHome manager.""" from collections.abc import Awaitable, Callable -from aioesphomeapi import ( - APIClient, - EntityInfo, - EntityState, - UserService, -) +from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService from homeassistant.components.esphome.const import DOMAIN, STABLE_BLE_VERSION_STR from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index ca97d9abeba..ffbe8f50e48 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -32,9 +32,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import ( - mock_platform, -) +from tests.common import mock_platform from tests.typing import WebSocketGenerator diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 83661a58280..e46906ffd33 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -18,11 +18,7 @@ from aioesphomeapi import ( ) from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ( - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index bd38f4d3302..d7b04f8448c 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -4,24 +4,12 @@ from collections.abc import Awaitable, Callable import dataclasses from unittest.mock import Mock, patch -from aioesphomeapi import ( - APIClient, - EntityInfo, - EntityState, - UserService, -) +from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService import pytest -from homeassistant.components.esphome.dashboard import ( - async_get_dashboard, -) +from homeassistant.components.esphome.dashboard import async_get_dashboard from homeassistant.components.update import UpdateEntityFeature -from homeassistant.const import ( - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 99ca7a3b6c5..dbff4713553 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -8,11 +8,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import ( - MOCK_FIRMWARE_AVAILABLE, - MOCK_FIRMWARE_RELEASE_URL, - MOCK_USER_DATA, -) +from .const import MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL, MOCK_USER_DATA from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator diff --git a/tests/components/gardena_bluetooth/__init__.py b/tests/components/gardena_bluetooth/__init__.py index 7de0780e129..5124daa7659 100644 --- a/tests/components/gardena_bluetooth/__init__.py +++ b/tests/components/gardena_bluetooth/__init__.py @@ -7,9 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from tests.common import MockConfigEntry -from tests.components.bluetooth import ( - inject_bluetooth_service_info, -) +from tests.components.bluetooth import inject_bluetooth_service_info WATER_TIMER_SERVICE_INFO = BluetoothServiceInfo( name="Timer", diff --git a/tests/components/gardena_bluetooth/test_button.py b/tests/components/gardena_bluetooth/test_button.py index 52fa3d4b00e..480f0c3572e 100644 --- a/tests/components/gardena_bluetooth/test_button.py +++ b/tests/components/gardena_bluetooth/test_button.py @@ -9,10 +9,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ( - ATTR_ENTITY_ID, - Platform, -) +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from . import setup_entry diff --git a/tests/components/gardena_bluetooth/test_config_flow.py b/tests/components/gardena_bluetooth/test_config_flow.py index 0f0e297c4d7..d533d1ff2da 100644 --- a/tests/components/gardena_bluetooth/test_config_flow.py +++ b/tests/components/gardena_bluetooth/test_config_flow.py @@ -17,9 +17,7 @@ from . import ( WATER_TIMER_UNNAMED_SERVICE_INFO, ) -from tests.components.bluetooth import ( - inject_bluetooth_service_info, -) +from tests.components.bluetooth import inject_bluetooth_service_info pytestmark = pytest.mark.usefixtures("mock_setup_entry") diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py index 3b04d0cc818..0003532fb60 100644 --- a/tests/components/gardena_bluetooth/test_number.py +++ b/tests/components/gardena_bluetooth/test_number.py @@ -19,10 +19,7 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - Platform, -) +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from . import setup_entry diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index c7e5005446f..41b6a9fc7dc 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -14,10 +14,7 @@ from homeassistant.setup import async_setup_component from .common import setup_test_component -from tests.common import ( - async_get_device_automations, - async_mock_service, -) +from tests.common import async_get_device_automations, async_mock_service @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index e89f53af73a..e79fce7ab13 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -11,10 +11,7 @@ from homeassistant.helpers import entity_registry as er from .conftest import setup_platform -from tests.common import ( - async_capture_events, - async_get_device_automations, -) +from tests.common import async_capture_events, async_get_device_automations async def test_hue_event( diff --git a/tests/components/hue/test_event.py b/tests/components/hue/test_event.py index 4dbb104357d..a3779c6b0e3 100644 --- a/tests/components/hue/test_event.py +++ b/tests/components/hue/test_event.py @@ -1,8 +1,5 @@ """Philips Hue Event platform tests for V2 bridge/api.""" -from homeassistant.components.event import ( - ATTR_EVENT_TYPE, - ATTR_EVENT_TYPES, -) +from homeassistant.components.event import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES from homeassistant.core import HomeAssistant from .conftest import setup_platform diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index a1234b7a470..b6d68714af5 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -8,12 +8,7 @@ import pytest import homeassistant.components.influxdb as influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET -from homeassistant.const import ( - PERCENTAGE, - STATE_OFF, - STATE_ON, - STATE_STANDBY, -) +from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON, STATE_STANDBY from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.setup import async_setup_component diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index 049f2a74250..fa29862d012 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -4,10 +4,7 @@ from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.lastfm.const import ( - CONF_USERS, - DOMAIN, -) +from homeassistant.components.lastfm.const import CONF_USERS, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index dcb52d68a79..05483b46d97 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -14,10 +14,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 3eba65dc8ab..221ae891d67 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -14,10 +14,7 @@ from homeassistant.components.lock import ( from homeassistant.const import ATTR_CODE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from .common import ( - set_node_attribute, - trigger_subscription_callback, -) +from .common import set_node_attribute, trigger_subscription_callback # This tests needs to be adjusted to remove lingering tasks diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index 911dd0fe389..0d5891a7778 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -5,16 +5,10 @@ from matter_server.client.models.node import MatterNode from matter_server.common.models import EventType, MatterNodeEvent import pytest -from homeassistant.components.event import ( - ATTR_EVENT_TYPE, - ATTR_EVENT_TYPES, -) +from homeassistant.components.event import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES from homeassistant.core import HomeAssistant -from .common import ( - setup_integration_with_node_fixture, - trigger_subscription_callback, -) +from .common import setup_integration_with_node_fixture, trigger_subscription_callback @pytest.fixture(name="generic_switch_node") diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 9d580da073e..9aa88c2d7ba 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -26,10 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.generated.mqtt import MQTT -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index bc7b8b43523..e78d3bd1d33 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -8,10 +8,7 @@ import pytest from homeassistant.components import event, mqtt from homeassistant.components.mqtt.event import MQTT_EVENT_ATTRIBUTES_BLOCKED -from homeassistant.const import ( - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -51,9 +48,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import ( - async_fire_mqtt_message, -) +from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 18269eb6970..0647721b4d0 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -12,10 +12,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers import ( - device_registry as dr, - issue_registry as ir, -) +from homeassistant.helpers import device_registry as dr, issue_registry as ir from tests.common import MockConfigEntry, async_capture_events, async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index a35b10afa9c..852075c6527 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -17,10 +17,7 @@ from homeassistant.util.dt import utcnow from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup -from tests.common import ( - async_get_device_automations, - async_mock_service, -) +from tests.common import async_get_device_automations, async_mock_service DEVICE_NAME = "My Camera" DATA_MESSAGE = {"message": "service-called"} diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index e785a5f3a8f..5dee2764cff 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -3,11 +3,7 @@ from typing import Any import pytest -from homeassistant.components.opensky.const import ( - CONF_ALTITUDE, - DEFAULT_NAME, - DOMAIN, -) +from homeassistant.components.opensky.const import CONF_ALTITUDE, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS from homeassistant.core import HomeAssistant diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 2abc27c8b14..49de6db6e13 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -9,10 +9,7 @@ from homeassistant.components.bluetooth import ( async_address_present, ) from homeassistant.components.oralb.const import DOMAIN -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_FRIENDLY_NAME, -) +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/tests/components/pegel_online/test_config_flow.py b/tests/components/pegel_online/test_config_flow.py index cb467e462f0..61f7dc75255 100644 --- a/tests/components/pegel_online/test_config_flow.py +++ b/tests/components/pegel_online/test_config_flow.py @@ -3,10 +3,7 @@ from unittest.mock import patch from aiohttp.client_exceptions import ClientError -from homeassistant.components.pegel_online.const import ( - CONF_STATION, - DOMAIN, -) +from homeassistant.components.pegel_online.const import CONF_STATION, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_LATITUDE, diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index 897bc5ebc70..0e8427b29e5 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -8,10 +8,7 @@ from homeassistant.components.philips_js.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import ( - async_get_device_automations, - async_mock_service, -) +from tests.common import async_get_device_automations, async_mock_service @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py index 78752372baa..9b83cd8c590 100644 --- a/tests/components/qingping/test_binary_sensor.py +++ b/tests/components/qingping/test_binary_sensor.py @@ -7,11 +7,7 @@ from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) from homeassistant.components.qingping.const import DOMAIN -from homeassistant.const import ( - ATTR_FRIENDLY_NAME, - STATE_OFF, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index 087a6840c59..6b2431fb763 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -16,10 +16,7 @@ from homeassistant.setup import async_setup_component from .conftest import create_rfx_test_cfg -from tests.common import ( - MockConfigEntry, - async_get_device_automations, -) +from tests.common import MockConfigEntry, async_get_device_automations class DeviceTestData(NamedTuple): diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 6e8c3bf8005..143501ef620 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -25,10 +25,7 @@ from homeassistant.setup import async_setup_component from . import init_integration -from tests.common import ( - MockConfigEntry, - async_get_device_automations, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.mark.parametrize( diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index a2628b11b84..67aa18ea75d 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -8,10 +8,7 @@ from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException from syrupy.assertion import SnapshotAssertion from homeassistant.components.smhi.const import ATTR_SMHI_THUNDER_PROBABILITY -from homeassistant.components.smhi.weather import ( - CONDITION_CLASSES, - RETRY_TIMEOUT, -) +from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEOUT from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, diff --git a/tests/components/subaru/test_device_tracker.py b/tests/components/subaru/test_device_tracker.py index 6bef5dc1c2c..616d868016e 100644 --- a/tests/components/subaru/test_device_tracker.py +++ b/tests/components/subaru/test_device_tracker.py @@ -9,11 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .api_responses import EXPECTED_STATE_EV_IMPERIAL, VEHICLE_STATUS_EV -from .conftest import ( - MOCK_API_FETCH, - MOCK_API_GET_DATA, - advance_time_to_next_fetch, -) +from .conftest import MOCK_API_FETCH, MOCK_API_GET_DATA, advance_time_to_next_fetch DEVICE_ID = "device_tracker.test_vehicle_2" diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index ffff4b1b8b0..190c56b33f6 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -18,10 +18,7 @@ from homeassistant.setup import async_setup_component from .test_common import DEFAULT_CONFIG, remove_device -from tests.common import ( - async_fire_mqtt_message, - async_get_device_automations, -) +from tests.common import async_fire_mqtt_message, async_get_device_automations from tests.typing import MqttMockHAClient, WebSocketGenerator diff --git a/tests/components/template/test_image.py b/tests/components/template/test_image.py index 17b84e327b1..7b399e13ec0 100644 --- a/tests/components/template/test_image.py +++ b/tests/components/template/test_image.py @@ -14,11 +14,7 @@ from homeassistant.components.input_text import ( DOMAIN as INPUT_TEXT_DOMAIN, SERVICE_SET_VALUE as INPUT_TEXT_SERVICE_SET_VALUE, ) -from homeassistant.const import ( - ATTR_ENTITY_PICTURE, - CONF_ENTITY_ID, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_ENTITY_PICTURE, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index 46aee182b53..5ec28c72fed 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -1,10 +1,7 @@ """The tests for the Transport NSW (AU) sensor platform.""" from unittest.mock import patch -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 89b65b1f981..0c6ac38739e 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -2,24 +2,13 @@ from aiounifi.websocket import WebsocketState -from homeassistant.components.button import ( - DOMAIN as BUTTON_DOMAIN, - ButtonDeviceClass, -) -from homeassistant.components.unifi.const import ( - DOMAIN as UNIFI_DOMAIN, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - STATE_UNAVAILABLE, - EntityCategory, -) +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass +from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .test_controller import ( - setup_unifi_integration, -) +from .test_controller import setup_unifi_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/unifi/test_image.py b/tests/components/unifi/test_image.py index afefee9fd02..38a8cef43c1 100644 --- a/tests/components/unifi/test_image.py +++ b/tests/components/unifi/test_image.py @@ -16,9 +16,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.util import dt as dt_util -from .test_controller import ( - setup_unifi_integration, -) +from .test_controller import setup_unifi_integration from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/weather/test_recorder.py b/tests/components/weather/test_recorder.py index 2864abf58bb..049a38cac1e 100644 --- a/tests/components/weather/test_recorder.py +++ b/tests/components/weather/test_recorder.py @@ -5,10 +5,7 @@ from datetime import timedelta from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states -from homeassistant.components.weather import ( - ATTR_CONDITION_SUNNY, - ATTR_FORECAST, -) +from homeassistant.components.weather import ATTR_CONDITION_SUNNY, ATTR_FORECAST from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 69adf890584..75a7834f629 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -6,10 +6,7 @@ from homeassistant.components.websocket_api.http import URL from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.typing import ( - MockHAClientWebSocket, - WebSocketGenerator, -) +from tests.typing import MockHAClientWebSocket, WebSocketGenerator @pytest.fixture diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 232362ce96f..3a68bbd88d3 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -33,10 +33,7 @@ from tests.common import ( async_mock_service, mock_platform, ) -from tests.typing import ( - ClientSessionGenerator, - WebSocketGenerator, -) +from tests.typing import ClientSessionGenerator, WebSocketGenerator STATE_KEY_SHORT_NAMES = { "entity_id": "e", diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index b94df47213e..e69b5629b63 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -18,10 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.typing import ( - MockHAClientWebSocket, - WebSocketGenerator, -) +from tests.typing import MockHAClientWebSocket, WebSocketGenerator @pytest.fixture diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index fd5db46e6c6..4ae8dcaddb1 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -17,10 +17,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import ( - async_get_device_automations, - async_mock_service, -) +from tests.common import async_get_device_automations, async_mock_service MOCK_DEVICE_ID = "some-device-id" DATA_MESSAGE = {"message": "service-called"} diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 46cdff180e9..9c44a0d08b5 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -19,11 +19,7 @@ from homeassistant.setup import async_setup_component from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE -from tests.common import ( - async_get_device_automations, - async_mock_service, - mock_coro, -) +from tests.common import async_get_device_automations, async_mock_service, mock_coro @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 8551427cf3e..fec9ec4cbbb 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -25,10 +25,7 @@ from homeassistant.helpers.device_registry import 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 ( - async_get_device_automations, - async_mock_service, -) +from tests.common import async_get_device_automations, async_mock_service @pytest.fixture diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 90c2238bb63..c6a9d59cb73 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -2,9 +2,7 @@ import pytest -from homeassistant.const import ( - UnitOfLength, -) +from homeassistant.const import UnitOfLength from homeassistant.exceptions import HomeAssistantError import homeassistant.util.distance as distance_util From bba57f39d55b8c23d14b0b1f6995fece7880a931 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 10 Aug 2023 15:00:43 +0200 Subject: [PATCH 105/179] Add Home Assistant Green (#98171) --- .github/workflows/builder.yml | 5 +++-- homeassistant/components/version/const.py | 1 + machine/green | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 machine/green diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 47e3e765b72..b5d37be44bc 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -197,7 +197,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.06.1 + uses: home-assistant/builder@2023.08.0 with: args: | $BUILD_ARGS \ @@ -251,6 +251,7 @@ jobs: - raspberrypi4-64 - tinker - yellow + - green steps: - name: Checkout the repository uses: actions/checkout@v3.5.3 @@ -274,7 +275,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.06.1 + uses: home-assistant/builder@2023.08.0 with: args: | $BUILD_ARGS \ diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index bdebf9f0255..2dcb0028b27 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -75,6 +75,7 @@ BOARD_MAP: Final[dict[str, str]] = { "Generic AArch64": "generic-aarch64", "Generic x86-64": "generic-x86-64", "Home Assistant Yellow": "yellow", + "Home Assistant Green": "green", "Khadas VIM3": "khadas-vim3", } diff --git a/machine/green b/machine/green new file mode 100644 index 00000000000..c1d74d3528e --- /dev/null +++ b/machine/green @@ -0,0 +1,4 @@ +ARG \ + BUILD_FROM + +FROM $BUILD_FROM From a7f7f56342ca2e621d00cb27373d814feba37382 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 15:42:46 +0200 Subject: [PATCH 106/179] Implement opensky data update coordinator (#97925) Co-authored-by: Robert Resch --- homeassistant/components/opensky/__init__.py | 7 +- homeassistant/components/opensky/const.py | 7 +- .../components/opensky/coordinator.py | 116 ++++++++++++++ homeassistant/components/opensky/sensor.py | 147 ++++-------------- .../opensky/snapshots/test_sensor.ambr | 2 + tests/components/opensky/test_init.py | 22 +++ 6 files changed, 180 insertions(+), 121 deletions(-) create mode 100644 homeassistant/components/opensky/coordinator.py diff --git a/homeassistant/components/opensky/__init__.py b/homeassistant/components/opensky/__init__.py index 197356b2092..81f348b5911 100644 --- a/homeassistant/components/opensky/__init__.py +++ b/homeassistant/components/opensky/__init__.py @@ -7,14 +7,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CLIENT, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS +from .coordinator import OpenSkyDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up opensky from a config entry.""" client = OpenSky(session=async_get_clientsession(hass)) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {CLIENT: client} + coordinator = OpenSkyDataUpdateCoordinator(hass, client) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/opensky/const.py b/homeassistant/components/opensky/const.py index ccea69f8b7f..4f4eb8a142c 100644 --- a/homeassistant/components/opensky/const.py +++ b/homeassistant/components/opensky/const.py @@ -1,11 +1,14 @@ """OpenSky constants.""" +import logging + from homeassistant.const import Platform +LOGGER = logging.getLogger(__package__) + PLATFORMS = [Platform.SENSOR] DEFAULT_NAME = "OpenSky" DOMAIN = "opensky" -CLIENT = "client" - +MANUFACTURER = "OpenSky Network" CONF_ALTITUDE = "altitude" ATTR_ICAO24 = "icao24" ATTR_CALLSIGN = "callsign" diff --git a/homeassistant/components/opensky/coordinator.py b/homeassistant/components/opensky/coordinator.py new file mode 100644 index 00000000000..1c3d10e0c33 --- /dev/null +++ b/homeassistant/components/opensky/coordinator.py @@ -0,0 +1,116 @@ +"""DataUpdateCoordinator for the OpenSky integration.""" +from __future__ import annotations + +from datetime import timedelta + +from python_opensky import OpenSky, OpenSkyError, StateVector + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_ALTITUDE, + ATTR_CALLSIGN, + ATTR_ICAO24, + ATTR_SENSOR, + CONF_ALTITUDE, + DEFAULT_ALTITUDE, + DOMAIN, + EVENT_OPENSKY_ENTRY, + EVENT_OPENSKY_EXIT, + LOGGER, +) + + +class OpenSkyDataUpdateCoordinator(DataUpdateCoordinator[int]): + """An OpenSky Data Update Coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, opensky: OpenSky) -> None: + """Initialize the OpenSky data coordinator.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + # OpenSky free user has 400 credits, with 4 credits per API call. 100/24 = ~4 requests per hour + update_interval=timedelta(minutes=15), + ) + self._opensky = opensky + self._previously_tracked: set[str] | None = None + self._bounding_box = OpenSky.get_bounding_box( + self.config_entry.data[CONF_LATITUDE], + self.config_entry.data[CONF_LONGITUDE], + self.config_entry.options[CONF_RADIUS], + ) + self._altitude = self.config_entry.options.get(CONF_ALTITUDE, DEFAULT_ALTITUDE) + + async def _async_update_data(self) -> int: + try: + response = await self._opensky.get_states(bounding_box=self._bounding_box) + except OpenSkyError as exc: + raise UpdateFailed from exc + currently_tracked = set() + flight_metadata: dict[str, StateVector] = {} + for flight in response.states: + if not flight.callsign: + continue + callsign = flight.callsign.strip() + if callsign: + flight_metadata[callsign] = flight + else: + continue + if ( + flight.longitude is None + or flight.latitude is None + or flight.on_ground + or flight.barometric_altitude is None + ): + continue + altitude = flight.barometric_altitude + if altitude > self._altitude and self._altitude != 0: + continue + currently_tracked.add(callsign) + if self._previously_tracked is not None: + entries = currently_tracked - self._previously_tracked + exits = self._previously_tracked - currently_tracked + self._handle_boundary(entries, EVENT_OPENSKY_ENTRY, flight_metadata) + self._handle_boundary(exits, EVENT_OPENSKY_EXIT, flight_metadata) + self._previously_tracked = currently_tracked + + return len(currently_tracked) + + def _handle_boundary( + self, flights: set[str], event: str, metadata: dict[str, StateVector] + ) -> None: + """Handle flights crossing region boundary.""" + for flight in flights: + if flight in metadata: + altitude = metadata[flight].barometric_altitude + longitude = metadata[flight].longitude + latitude = metadata[flight].latitude + icao24 = metadata[flight].icao24 + else: + # Assume Flight has landed if missing. + altitude = 0 + longitude = None + latitude = None + icao24 = None + + data = { + ATTR_CALLSIGN: flight, + ATTR_ALTITUDE: altitude, + ATTR_SENSOR: self.config_entry.title, + ATTR_LONGITUDE: longitude, + ATTR_LATITUDE: latitude, + ATTR_ICAO24: icao24, + } + self.hass.bus.fire(event, data) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 4ef1070d12d..3c0340594a9 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -1,16 +1,15 @@ """Sensor for the Open Sky Network.""" from __future__ import annotations -from datetime import timedelta - -from python_opensky import BoundingBox, OpenSky, StateVector import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, @@ -18,26 +17,20 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - ATTR_ALTITUDE, - ATTR_CALLSIGN, - ATTR_ICAO24, - ATTR_SENSOR, - CLIENT, CONF_ALTITUDE, DEFAULT_ALTITUDE, DOMAIN, - EVENT_OPENSKY_ENTRY, - EVENT_OPENSKY_EXIT, + MANUFACTURER, ) - -# OpenSky free user has 400 credits, with 4 credits per API call. 100/24 = ~4 requests per hour -SCAN_INTERVAL = timedelta(minutes=15) - +from .coordinator import OpenSkyDataUpdateCoordinator PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,125 +80,45 @@ async def async_setup_entry( ) -> None: """Initialize the entries.""" - opensky = hass.data[DOMAIN][entry.entry_id][CLIENT] - bounding_box = OpenSky.get_bounding_box( - entry.data[CONF_LATITUDE], - entry.data[CONF_LONGITUDE], - entry.options[CONF_RADIUS], - ) + coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ OpenSkySensor( - entry.title, - opensky, - bounding_box, - entry.options.get(CONF_ALTITUDE, DEFAULT_ALTITUDE), - entry.entry_id, + coordinator, + entry, ) ], - True, ) -class OpenSkySensor(SensorEntity): +class OpenSkySensor(CoordinatorEntity[OpenSkyDataUpdateCoordinator], SensorEntity): """Open Sky Network Sensor.""" _attr_attribution = ( "Information provided by the OpenSky Network (https://opensky-network.org)" ) + _attr_has_entity_name = True + _attr_name = None + _attr_icon = "mdi:airplane" + _attr_native_unit_of_measurement = "flights" + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, - name: str, - opensky: OpenSky, - bounding_box: BoundingBox, - altitude: float, - entry_id: str, + coordinator: OpenSkyDataUpdateCoordinator, + config_entry: ConfigEntry, ) -> None: """Initialize the sensor.""" - self._altitude = altitude - self._state = 0 - self._name = name - self._previously_tracked: set[str] = set() - self._opensky = opensky - self._bounding_box = bounding_box - self._attr_unique_id = f"{entry_id}_opensky" - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name + super().__init__(coordinator) + self._attr_unique_id = f"{config_entry.entry_id}_opensky" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}")}, + manufacturer=MANUFACTURER, + name=config_entry.title, + entry_type=DeviceEntryType.SERVICE, + ) @property def native_value(self) -> int: """Return the state of the sensor.""" - return self._state - - def _handle_boundary( - self, flights: set[str], event: str, metadata: dict[str, StateVector] - ) -> None: - """Handle flights crossing region boundary.""" - for flight in flights: - if flight in metadata: - altitude = metadata[flight].barometric_altitude - longitude = metadata[flight].longitude - latitude = metadata[flight].latitude - icao24 = metadata[flight].icao24 - else: - # Assume Flight has landed if missing. - altitude = 0 - longitude = None - latitude = None - icao24 = None - - data = { - ATTR_CALLSIGN: flight, - ATTR_ALTITUDE: altitude, - ATTR_SENSOR: self._name, - ATTR_LONGITUDE: longitude, - ATTR_LATITUDE: latitude, - ATTR_ICAO24: icao24, - } - self.hass.bus.fire(event, data) - - async def async_update(self) -> None: - """Update device state.""" - currently_tracked = set() - flight_metadata: dict[str, StateVector] = {} - response = await self._opensky.get_states(bounding_box=self._bounding_box) - for flight in response.states: - if not flight.callsign: - continue - callsign = flight.callsign.strip() - if callsign != "": - flight_metadata[callsign] = flight - else: - continue - if ( - flight.longitude is None - or flight.latitude is None - or flight.on_ground - or flight.barometric_altitude is None - ): - continue - altitude = flight.barometric_altitude - if altitude > self._altitude and self._altitude != 0: - continue - currently_tracked.add(callsign) - if self._previously_tracked is not None: - entries = currently_tracked - self._previously_tracked - exits = self._previously_tracked - currently_tracked - self._handle_boundary(entries, EVENT_OPENSKY_ENTRY, flight_metadata) - self._handle_boundary(exits, EVENT_OPENSKY_EXIT, flight_metadata) - self._state = len(currently_tracked) - self._previously_tracked = currently_tracked - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return "flights" - - @property - def icon(self) -> str: - """Return the icon.""" - return "mdi:airplane" + return self.coordinator.data diff --git a/tests/components/opensky/snapshots/test_sensor.ambr b/tests/components/opensky/snapshots/test_sensor.ambr index 1bd85d23400..a57b438df67 100644 --- a/tests/components/opensky/snapshots/test_sensor.ambr +++ b/tests/components/opensky/snapshots/test_sensor.ambr @@ -5,6 +5,7 @@ 'attribution': 'Information provided by the OpenSky Network (https://opensky-network.org)', 'friendly_name': 'OpenSky', 'icon': 'mdi:airplane', + 'state_class': , 'unit_of_measurement': 'flights', }), 'context': , @@ -20,6 +21,7 @@ 'attribution': 'Information provided by the OpenSky Network (https://opensky-network.org)', 'friendly_name': 'OpenSky', 'icon': 'mdi:airplane', + 'state_class': , 'unit_of_measurement': 'flights', }), 'context': , diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py index be1c21627f0..961aaab61fc 100644 --- a/tests/components/opensky/test_init.py +++ b/tests/components/opensky/test_init.py @@ -1,8 +1,14 @@ """Test OpenSky component setup process.""" from __future__ import annotations +from unittest.mock import patch + +from python_opensky import OpenSkyError + from homeassistant.components.opensky.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from .conftest import ComponentSetup @@ -26,3 +32,19 @@ async def test_load_unload_entry( state = hass.states.get("sensor.opensky") assert not state + + +async def test_load_entry_failure( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test failure while loading.""" + config_entry.add_to_hass(hass) + with patch( + "python_opensky.OpenSky.get_states", + side_effect=OpenSkyError(), + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.state == ConfigEntryState.SETUP_RETRY From 59768635f2688cde3f36431fe53f6cec9b035c83 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 16:04:00 +0200 Subject: [PATCH 107/179] Fix ruff checks for opensky (#98176) --- homeassistant/components/opensky/sensor.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 3c0340594a9..a890d022e0a 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -9,12 +9,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType @@ -24,12 +19,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - CONF_ALTITUDE, - DEFAULT_ALTITUDE, - DOMAIN, - MANUFACTURER, -) +from .const import CONF_ALTITUDE, DEFAULT_ALTITUDE, DOMAIN, MANUFACTURER from .coordinator import OpenSkyDataUpdateCoordinator PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( From 4981eadd3114fdcf39b52438a61a84b92e8d472d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 10 Aug 2023 16:47:16 +0200 Subject: [PATCH 108/179] Update aioairzone to v0.6.5 (#98163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_climate.py | 49 ++++++++++++++++++- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 88b918f699c..39adf08236e 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.6.4"] + "requirements": ["aioairzone==0.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9cd4277ee74..81ebc60d3ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.4 +aioairzone==0.6.5 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f67fdd61e15..ed6d10bd40d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -172,7 +172,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.4 +aioairzone==0.6.5 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index f7cc7806bcb..3e68c056566 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -329,7 +329,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: await async_init_integration(hass) - HVAC_MOCK = { + HVAC_MOCK_1 = { API_DATA: [ { API_SYSTEM_ID: 1, @@ -340,7 +340,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: } with patch( "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", - return_value=HVAC_MOCK, + return_value=HVAC_MOCK_1, ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -407,6 +407,51 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: state = hass.states.get("climate.airzone_2_1") assert state.state == HVACMode.HEAT_COOL + HVAC_MOCK_4 = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_ON: 1, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK_4, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVACMode.FAN_ONLY, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVACMode.FAN_ONLY + + HVAC_MOCK_NO_SET_POINT = {**HVAC_MOCK} + del HVAC_MOCK_NO_SET_POINT[API_SYSTEMS][0][API_DATA][0][API_SET_POINT] + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK_NO_SET_POINT, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + return_value=HVAC_SYSTEMS_MOCK, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ): + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("climate.salon") + assert state.attributes.get(ATTR_TEMPERATURE) == 19.1 + async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None: """Test setting the HVAC mode for a slave zone.""" From 9a2cb3ad1fc00a1192baf2db5fa0e31c390d622e Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 10 Aug 2023 07:49:23 -0700 Subject: [PATCH 109/179] Opower: Add gas sensors for utilities that report CCF (#98142) Add gas sensors for utilities that report CCF --- homeassistant/components/opower/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index 36f88a36e8a..ad94d8cafb6 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -188,7 +188,7 @@ async def async_setup_entry( sensors = ELEC_SENSORS elif ( forecast.account.meter_type == MeterType.GAS - and forecast.unit_of_measure == UnitOfMeasure.THERM + and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF] ): sensors = GAS_SENSORS for sensor in sensors: From 6545c46ba05019ab85c99e388d6dd8d698ca6626 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 10 Aug 2023 14:54:19 +0000 Subject: [PATCH 110/179] Bump pynina to 0.3.2 (#98070) --- homeassistant/components/nina/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index d1897b53e04..df09d168827 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], - "requirements": ["PyNINA==0.3.1"] + "requirements": ["PyNINA==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 81ebc60d3ac..bdd5ebce16f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -79,7 +79,7 @@ PyMetno==0.10.0 PyMicroBot==0.0.9 # homeassistant.components.nina -PyNINA==0.3.1 +PyNINA==0.3.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed6d10bd40d..341c2371c7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ PyMetno==0.10.0 PyMicroBot==0.0.9 # homeassistant.components.nina -PyNINA==0.3.1 +PyNINA==0.3.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks From 07a701551be132ba4eff8097f90f8b03e43fedce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 18:04:24 +0200 Subject: [PATCH 111/179] Add missing translation key in Tuya (#98122) --- homeassistant/components/tuya/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 676991fe167..a48d797555c 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -105,11 +105,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { translation_key="plug", ), ), - # Cirquit Breaker + # Circuit Breaker "dlq": ( SwitchEntityDescription( key=DPCode.CHILD_LOCK, - translation_key="asd", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), From f0e9dccb5816438377282c30facdc12ed5009d18 Mon Sep 17 00:00:00 2001 From: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:11:15 +0200 Subject: [PATCH 112/179] Only handle shell commands output when return_response requested (#97777) --- .../components/shell_command/__init__.py | 27 ++++++++++----- tests/components/shell_command/test_init.py | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 8430d7284ee..b2f38f54b20 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -105,14 +105,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: raise - service_response: JsonObjectType = { - "stdout": "", - "stderr": "", - "returncode": process.returncode, - } - if stdout_data: - service_response["stdout"] = stdout_data.decode("utf-8").strip() _LOGGER.debug( "Stdout of command: `%s`, return code: %s:\n%s", cmd, @@ -120,7 +113,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: stdout_data, ) if stderr_data: - service_response["stderr"] = stderr_data.decode("utf-8").strip() _LOGGER.debug( "Stderr of command: `%s`, return code: %s:\n%s", cmd, @@ -132,7 +124,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "Error running command: `%s`, return code: %s", cmd, process.returncode ) - return service_response + if service.return_response: + service_response: JsonObjectType = { + "stdout": "", + "stderr": "", + "returncode": process.returncode, + } + try: + if stdout_data: + service_response["stdout"] = stdout_data.decode("utf-8").strip() + if stderr_data: + service_response["stderr"] = stderr_data.decode("utf-8").strip() + return service_response + except UnicodeDecodeError: + _LOGGER.exception( + "Unable to handle non-utf8 output of command: `%s`", cmd + ) + raise + return None for name in conf: hass.services.async_register( diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index ac594c811ed..1efcc9dc919 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -174,6 +174,40 @@ async def test_stdout_captured(mock_output, hass: HomeAssistant) -> None: assert response["returncode"] == 0 +@patch("homeassistant.components.shell_command._LOGGER.debug") +async def test_non_text_stdout_capture( + mock_output, hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling of non-text output.""" + assert await async_setup_component( + hass, + shell_command.DOMAIN, + { + shell_command.DOMAIN: { + "output_image": "curl -o - https://raw.githubusercontent.com/home-assistant/assets/master/misc/loading-screen.gif" + } + }, + ) + + # No problem without 'return_response' + response = await hass.services.async_call( + "shell_command", "output_image", blocking=True + ) + + await hass.async_block_till_done() + assert not response + + # Non-text output throws with 'return_response' + with pytest.raises(UnicodeDecodeError): + response = await hass.services.async_call( + "shell_command", "output_image", blocking=True, return_response=True + ) + + await hass.async_block_till_done() + assert not response + assert "Unable to handle non-utf8 output of command" in caplog.text + + @patch("homeassistant.components.shell_command._LOGGER.debug") async def test_stderr_captured(mock_output, hass: HomeAssistant) -> None: """Test subprocess that has stderr.""" From f77387bd0f5d44ca086909203f09b25ee8d67711 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:21:46 +0200 Subject: [PATCH 113/179] Adjust asuswrt tests which create devices (#98182) --- tests/components/asuswrt/test_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 2d7bda491a8..52525390666 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -95,6 +95,7 @@ def create_device_registry_devices_fixture(hass: HomeAssistant): """Create device registry devices so the device tracker entities are enabled when added.""" dev_reg = dr.async_get(hass) config_entry = MockConfigEntry(domain="something_else") + config_entry.add_to_hass(hass) for idx, device in enumerate( ( From 5909a1187de5a767f20db619019b8ce330eac44b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:22:12 +0200 Subject: [PATCH 114/179] Adjust config tests which create devices (#98184) --- tests/components/config/test_device_registry.py | 16 ++++++++++------ tests/components/config/test_entity_registry.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 25b465192cf..a92b2a353ef 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -26,15 +26,17 @@ async def test_list_devices( hass: HomeAssistant, client, device_registry: dr.DeviceRegistry ) -> None: """Test list entries.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) device1 = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) device2 = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, identifiers={("bridgeid", "1234")}, manufacturer="manufacturer", model="model", @@ -50,7 +52,7 @@ async def test_list_devices( assert msg["result"] == [ { "area_id": None, - "config_entries": ["1234"], + "config_entries": [entry.entry_id], "configuration_url": None, "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], "disabled_by": None, @@ -66,7 +68,7 @@ async def test_list_devices( }, { "area_id": None, - "config_entries": ["1234"], + "config_entries": [entry.entry_id], "configuration_url": None, "connections": [], "disabled_by": None, @@ -94,7 +96,7 @@ async def test_list_devices( assert msg["result"] == [ { "area_id": None, - "config_entries": ["1234"], + "config_entries": [entry.entry_id], "configuration_url": None, "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], "disabled_by": None, @@ -135,8 +137,10 @@ async def test_update_device( payload_value, ) -> None: """Test update entry.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) device = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9cad68c9c99..a002f2c2d50 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -721,7 +721,7 @@ async def test_enable_entity_disabled_device( config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=config_entry.entry_id, connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", From da53944a37792960d0b0932f73697da9161620d9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:22:17 +0200 Subject: [PATCH 115/179] Adjust conversation tests which create devices (#98185) --- tests/components/conversation/test_default_agent.py | 8 ++++++-- tests/components/conversation/test_init.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index c3c2e621260..1677b254ff6 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -20,7 +20,7 @@ from homeassistant.setup import async_setup_component from . import expose_entity -from tests.common import async_mock_service +from tests.common import MockConfigEntry, async_mock_service @pytest.fixture @@ -86,8 +86,12 @@ async def test_exposed_areas( area_kitchen = area_registry.async_get_or_create("kitchen") area_bedroom = area_registry.async_get_or_create("bedroom") + entry = MockConfigEntry() + entry.add_to_hass(hass) kitchen_device = device_registry.async_get_or_create( - config_entry_id="1234", connections=set(), identifiers={("demo", "id-1234")} + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, ) device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index f89af1dc201..37c8f9401bc 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1409,6 +1409,7 @@ async def test_turn_on_area( ) -> None: """Test turning on an area.""" entry = MockConfigEntry(domain="test") + entry.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -1480,6 +1481,7 @@ async def test_light_area_same_name( ) -> None: """Test turning on a light with the same name as an area.""" entry = MockConfigEntry(domain="test") + entry.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, From ec143b26d7052959c0c4bbd82310f9bd4de52d13 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:22:33 +0200 Subject: [PATCH 116/179] Adjust device_tracker tests which create devices (#98188) --- tests/components/device_tracker/test_entities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index 4ea5d280b5b..960f9c18b08 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -25,9 +25,11 @@ async def test_scanner_entity_device_tracker( ) -> None: """Test ScannerEntity based device tracker.""" # Make device tied to other integration so device tracker entities get enabled + other_config_entry = MockConfigEntry(domain="not_fake_integration") + other_config_entry.add_to_hass(hass) dr.async_get(hass).async_get_or_create( name="Device from other integration", - config_entry_id=MockConfigEntry().entry_id, + config_entry_id=other_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "ad:de:ef:be:ed:fe")}, ) From 52183d64ae56fc7c8e7a50481e3ecc4b98d2b2a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:22:39 +0200 Subject: [PATCH 117/179] Adjust fibaro tests which create devices (#98189) --- tests/components/fibaro/conftest.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index 056b23e1cf4..8a2bbcbcd4a 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -6,10 +6,12 @@ from pyfibaro.fibaro_scene import SceneModel import pytest from homeassistant.components.fibaro import DOMAIN, FIBARO_CONTROLLER, FIBARO_DEVICES -from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: @@ -39,13 +41,8 @@ async def setup_platform( ) -> ConfigEntry: """Set up the fibaro platform and prerequisites.""" hass.config.components.add(DOMAIN) - config_entry = ConfigEntry( - 1, - DOMAIN, - "Test", - {}, - SOURCE_USER, - ) + config_entry = MockConfigEntry(domain=DOMAIN, title="Test") + config_entry.add_to_hass(hass) controller_mock = Mock() controller_mock.hub_serial = "HC2-111111" From 983ebeff809c73a0fb8a7ffba21fb185e9ad1e27 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:22:44 +0200 Subject: [PATCH 118/179] Adjust freebox tests which create devices (#98190) --- tests/components/freebox/conftest.py | 4 +++- tests/components/freebox/test_button.py | 6 ++++-- tests/components/freebox/test_init.py | 8 +++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index b950d44508d..69b250412bd 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import ( @@ -27,9 +28,10 @@ def mock_path(): @pytest.fixture -def mock_device_registry_devices(device_registry): +def mock_device_registry_devices(hass: HomeAssistant, device_registry): """Create device registry devices so the device tracker entities are enabled.""" config_entry = MockConfigEntry(domain="something_else") + config_entry.add_to_hass(hass) for idx, device in enumerate( ( diff --git a/tests/components/freebox/test_button.py b/tests/components/freebox/test_button.py index aabf4682832..de15e90f54f 100644 --- a/tests/components/freebox/test_button.py +++ b/tests/components/freebox/test_button.py @@ -1,5 +1,7 @@ """Tests for the Freebox config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch + +from pytest_unordered import unordered from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.freebox.const import DOMAIN @@ -22,7 +24,7 @@ async def test_reboot_button(hass: HomeAssistant, router: Mock) -> None: entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert hass.config_entries.async_entries() == [entry] + assert hass.config_entries.async_entries() == unordered([entry, ANY]) assert router.call_count == 1 assert router().open.call_count == 1 diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index 6197f03b0ec..85acfdccc4d 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -1,5 +1,7 @@ """Tests for the Freebox config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch + +from pytest_unordered import unordered from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN from homeassistant.components.freebox.const import DOMAIN, SERVICE_REBOOT @@ -25,7 +27,7 @@ async def test_setup(hass: HomeAssistant, router: Mock) -> None: entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert hass.config_entries.async_entries() == [entry] + assert hass.config_entries.async_entries() == unordered([entry, ANY]) assert router.call_count == 1 assert router().open.call_count == 1 @@ -57,7 +59,7 @@ async def test_setup_import(hass: HomeAssistant, router: Mock) -> None: hass, DOMAIN, {DOMAIN: {CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}} ) await hass.async_block_till_done() - assert hass.config_entries.async_entries() == [entry] + assert hass.config_entries.async_entries() == unordered([entry, ANY]) assert router.call_count == 1 assert router().open.call_count == 1 From f11f7ac45c0d54e5324824f8a0a8773585394e42 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:10 +0200 Subject: [PATCH 119/179] Adjust google_assistant tests which create devices (#98191) --- tests/components/google_assistant/test_smart_home.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index f471e6f862c..6cfa7965074 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -39,7 +39,7 @@ from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig -from tests.common import async_capture_events +from tests.common import MockConfigEntry, async_capture_events REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -251,10 +251,12 @@ async def test_sync_message(hass: HomeAssistant, registries) -> None: @pytest.mark.parametrize("area_on_device", [True, False]) async def test_sync_in_area(area_on_device, hass: HomeAssistant, registries) -> None: """Test a sync message where room hint comes from area.""" + entry = MockConfigEntry() + entry.add_to_hass(hass) area = registries.area.async_create("Living Room") device = registries.device.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, manufacturer="Someone", model="Some model", sw_version="Some Version", @@ -625,8 +627,8 @@ async def test_execute_times_out( """Test an execute command which times out.""" orig_execute_limit = sh.EXECUTE_LIMIT sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms - await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) await async_setup_component(hass, "homeassistant", {}) + await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) await hass.async_block_till_done() await hass.services.async_call( From 4329a47ef80ee2703deb81297c5d694531564bb2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:13 +0200 Subject: [PATCH 120/179] Adjust google_generative_ai_conversation tests which create devices (#98192) --- .../test_init.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index e8da4cf3920..982f3993e04 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -20,11 +20,13 @@ async def test_default_prompt( snapshot: SnapshotAssertion, ) -> None: """Test that the default prompt works.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) for i in range(3): area_registry.async_create(f"{i}Empty Area") device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "1234")}, name="Test Device", manufacturer="Test Manufacturer", @@ -33,7 +35,7 @@ async def test_default_prompt( ) for i in range(3): device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", f"{i}abcd")}, name="Test Service", manufacturer="Test Manufacturer", @@ -42,7 +44,7 @@ async def test_default_prompt( entry_type=dr.DeviceEntryType.SERVICE, ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "5678")}, name="Test Device 2", manufacturer="Test Manufacturer 2", @@ -50,7 +52,7 @@ async def test_default_prompt( suggested_area="Test Area 2", ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876")}, name="Test Device 3", manufacturer="Test Manufacturer 3", @@ -58,13 +60,13 @@ async def test_default_prompt( suggested_area="Test Area 2", ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "qwer")}, name="Test Device 4", suggested_area="Test Area 2", ) device = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876-disabled")}, name="Test Device 3", manufacturer="Test Manufacturer 3", @@ -75,14 +77,14 @@ async def test_default_prompt( device.id, disabled_by=dr.DeviceEntryDisabler.USER ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876-no-name")}, manufacturer="Test Manufacturer NoName", model="Test Model NoName", suggested_area="Test Area 2", ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876-integer-values")}, name=1, manufacturer=2, From 07b19b3dd91d3b9bd5dcaff51eb71dce86135cb5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:17 +0200 Subject: [PATCH 121/179] Adjust homekit tests which create devices (#98193) --- tests/components/homekit/test_homekit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 109f4205901..02807ba6557 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1019,6 +1019,7 @@ async def test_homekit_unpair_not_homekit_device( not_homekit_entry = MockConfigEntry( domain="not_homekit", data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) + not_homekit_entry.add_to_hass(hass) entity_id = "light.demo" hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) From 3e6c844048a62002ff4e01d5da8d37470aae0cf9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:33 +0200 Subject: [PATCH 122/179] Adjust integration tests which create devices (#98196) --- tests/components/integration/test_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index a552d401681..0c2744dd654 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -685,6 +685,7 @@ async def test_device_id(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, From 3495d762a49d2172b9ec8decd5a859643d19bc21 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:37 +0200 Subject: [PATCH 123/179] Adjust kraken tests which create devices (#98197) --- tests/components/kraken/test_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index b8f1f165069..1435e0d6b04 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -248,6 +248,7 @@ async def test_sensors_available_after_restart(hass: HomeAssistant) -> None: CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR], }, ) + entry.add_to_hass(hass) device_registry = dr.async_get(hass) device_registry.async_get_or_create( From e9a0436605b8058e26d5099b784c1caaa70e92ac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:41 +0200 Subject: [PATCH 124/179] Adjust matter tests which create devices (#98198) --- tests/components/matter/test_helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py index 28f4479432c..36761362618 100644 --- a/tests/components/matter/test_helpers.py +++ b/tests/components/matter/test_helpers.py @@ -43,6 +43,7 @@ async def test_get_node_from_device_entry( device_registry = dr.async_get(hass) other_domain = "other_domain" other_config_entry = MockConfigEntry(domain=other_domain) + other_config_entry.add_to_hass(hass) other_device_entry = device_registry.async_get_or_create( config_entry_id=other_config_entry.entry_id, identifiers={(other_domain, "1234")}, From 9831498259f55d0cc52692fa118c1280064797eb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:44 +0200 Subject: [PATCH 125/179] Adjust mazda tests which create devices (#98199) --- tests/components/mazda/test_init.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 86436ac4184..3556f687989 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -259,8 +259,10 @@ async def test_service_device_id_not_mazda_vehicle(hass: HomeAssistant) -> None: device_registry = dr.async_get(hass) # Create another device and pass its device ID. # Service should fail because device is from wrong domain. + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) other_device = device_registry.async_get_or_create( - config_entry_id="test_config_entry_id", + config_entry_id=other_config_entry.entry_id, identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")}, ) From bd4d1abc28d7a1293b708e0a83d7f2b49cab4adb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:48 +0200 Subject: [PATCH 126/179] Adjust mikrotik tests which create devices (#98200) --- tests/components/mikrotik/test_device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 323c958eb22..84fcfabffee 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -35,6 +35,7 @@ def mock_device_registry_devices(hass: HomeAssistant) -> None: """Create device registry devices so the device tracker entities are enabled.""" dev_reg = dr.async_get(hass) config_entry = MockConfigEntry(domain="something_else") + config_entry.add_to_hass(hass) for idx, device in enumerate( ( From 0b69f37d57892fbadc6faf7f3998d38a8a982fdc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:51 +0200 Subject: [PATCH 127/179] Adjust motioneye tests which create devices (#98201) --- tests/components/motioneye/test_web_hooks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index a55f71b1d60..617f472ab4e 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -46,7 +46,7 @@ from . import ( setup_mock_motioneye_config_entry, ) -from tests.common import async_capture_events, async_fire_time_changed +from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed from tests.typing import ClientSessionGenerator WEB_HOOK_MOTION_DETECTED_QUERY_STRING = ( @@ -469,8 +469,10 @@ async def test_event_media_data( assert "media_content_id" not in events[-1].data # Test: Not a loaded motionEye config entry. + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) wrong_device = device_registry.async_get_or_create( - config_entry_id="wrong_config_id", identifiers={("motioneye", "a_1")} + config_entry_id=other_config_entry.entry_id, identifiers={("motioneye", "a_1")} ) resp = await hass_client.post( URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), From 13a9b83ed35cb28fb00628d8fe8a93ae9b3cf37f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:23:55 +0200 Subject: [PATCH 128/179] Adjust mqtt tests which create devices (#98202) --- tests/components/mqtt/test_event.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index e78d3bd1d33..abcd6e8f3ee 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -48,7 +48,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message +from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient DEFAULT_CONFIG = { @@ -503,9 +503,11 @@ async def test_entity_device_info_with_hub( ) -> None: """Test MQTT event device registry integration.""" await mqtt_mock_entry() + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) registry = dr.async_get(hass) hub = registry.async_get_or_create( - config_entry_id="123", + config_entry_id=other_config_entry.entry_id, connections=set(), identifiers={("mqtt", "hub-id")}, manufacturer="manufacturer", From 9d3be60b056b29408d4ef04b896ee27c8ea8ceee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:19 +0200 Subject: [PATCH 129/179] Adjust openai_conversation tests which create devices (#98203) --- .../openai_conversation/test_init.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 1b9f81f60c0..1b145d9d545 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -22,11 +22,13 @@ async def test_default_prompt( snapshot: SnapshotAssertion, ) -> None: """Test that the default prompt works.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) for i in range(3): area_registry.async_create(f"{i}Empty Area") device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "1234")}, name="Test Device", manufacturer="Test Manufacturer", @@ -35,7 +37,7 @@ async def test_default_prompt( ) for i in range(3): device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", f"{i}abcd")}, name="Test Service", manufacturer="Test Manufacturer", @@ -44,7 +46,7 @@ async def test_default_prompt( entry_type=dr.DeviceEntryType.SERVICE, ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "5678")}, name="Test Device 2", manufacturer="Test Manufacturer 2", @@ -52,7 +54,7 @@ async def test_default_prompt( suggested_area="Test Area 2", ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876")}, name="Test Device 3", manufacturer="Test Manufacturer 3", @@ -60,13 +62,13 @@ async def test_default_prompt( suggested_area="Test Area 2", ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "qwer")}, name="Test Device 4", suggested_area="Test Area 2", ) device = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876-disabled")}, name="Test Device 3", manufacturer="Test Manufacturer 3", @@ -77,14 +79,14 @@ async def test_default_prompt( device.id, disabled_by=dr.DeviceEntryDisabler.USER ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876-no-name")}, manufacturer="Test Manufacturer NoName", model="Test Model NoName", suggested_area="Test Area 2", ) device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=entry.entry_id, connections={("test", "9876-integer-values")}, name=1, manufacturer=2, From 7b157baed6b788041d378c575396d9e56005039f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:22 +0200 Subject: [PATCH 130/179] Adjust plex tests which create devices (#98204) --- tests/components/plex/test_device_handling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/plex/test_device_handling.py b/tests/components/plex/test_device_handling.py index 6abdc8cbeca..5887079ce21 100644 --- a/tests/components/plex/test_device_handling.py +++ b/tests/components/plex/test_device_handling.py @@ -15,6 +15,7 @@ async def test_cleanup_orphaned_devices( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) + entry.add_to_hass(hass) test_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -55,6 +56,7 @@ async def test_migrate_transient_devices( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) + entry.add_to_hass(hass) # Pre-create devices and entities to test device migration plexweb_device = device_registry.async_get_or_create( From 6ea7011a07c83d556f207926ce03715378a5a08d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:25 +0200 Subject: [PATCH 131/179] Adjust ps4 tests which create devices (#98205) --- tests/components/ps4/test_media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index acb84186c0b..74b13d2f909 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -325,6 +325,7 @@ async def test_device_info_is_assummed( ) -> None: """Test that device info is assumed if device is unavailable.""" # Create a device registry entry with device info. + MOCK_CONFIG.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=MOCK_ENTRY_ID, name=MOCK_HOST_NAME, From 8813140ed5b1d73a2e332e1332f45f8f0bd684a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:36 +0200 Subject: [PATCH 132/179] Adjust threshold tests which create devices (#98208) --- tests/components/threshold/test_binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index e26781029c5..c4b1dad78d5 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -597,6 +597,7 @@ async def test_device_id(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, From fb1bb0d37473df4008ec6dce7a22c49bd34288a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:39 +0200 Subject: [PATCH 133/179] Adjust switch_as_x tests which create devices (#98210) --- tests/components/switch_as_x/test_init.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index fac744d0c0e..a0c0bfca825 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -143,6 +143,7 @@ async def test_device_registry_config_entry_1( entity_registry = er.async_get(hass) switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=switch_config_entry.entry_id, @@ -170,7 +171,6 @@ async def test_device_registry_config_entry_1( }, title="ABC", ) - switch_as_x_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) @@ -202,6 +202,7 @@ async def test_device_registry_config_entry_2( entity_registry = er.async_get(hass) switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=switch_config_entry.entry_id, @@ -313,6 +314,7 @@ async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: entity_registry = er.async_get(hass) test_config_entry = MockConfigEntry() + test_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=test_config_entry.entry_id, @@ -504,6 +506,7 @@ async def test_entity_name( device_registry = dr.async_get(hass) switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=switch_config_entry.entry_id, @@ -559,6 +562,7 @@ async def test_custom_name_1( device_registry = dr.async_get(hass) switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=switch_config_entry.entry_id, @@ -622,6 +626,7 @@ async def test_custom_name_2( device_registry = dr.async_get(hass) switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=switch_config_entry.entry_id, From 05ac67eba2be3c6bedb990eae466c46b2b51487c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:42 +0200 Subject: [PATCH 134/179] Adjust unifi tests which create devices (#98211) --- tests/components/unifi/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 19574a9ab42..ca0c855d1ab 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -59,6 +59,7 @@ def mock_device_registry(hass): """Mock device registry.""" dev_reg = dr.async_get(hass) config_entry = MockConfigEntry(domain="something_else") + config_entry.add_to_hass(hass) for idx, device in enumerate( ( From c1b47b88f2bdf560bec5435dd8e88a0b9536b5ea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:24:47 +0200 Subject: [PATCH 135/179] Adjust utility_meter tests which create devices (#98212) --- tests/components/utility_meter/test_config_flow.py | 2 ++ tests/components/utility_meter/test_sensor.py | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 88a77407c07..262dbf36306 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -277,6 +277,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: # Configure source entity 1 (with a linked device) source_config_entry_1 = MockConfigEntry() + source_config_entry_1.add_to_hass(hass) source_device_entry_1 = device_registry.async_get_or_create( config_entry_id=source_config_entry_1.entry_id, identifiers={("sensor", "identifier_test1")}, @@ -292,6 +293,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: # Configure source entity 2 (with a linked device) source_config_entry_2 = MockConfigEntry() + source_config_entry_2.add_to_hass(hass) source_device_entry_2 = device_registry.async_get_or_create( config_entry_id=source_config_entry_2.entry_id, identifiers={("sensor", "identifier_test2")}, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 3d2d95fd26f..b8f197a4dee 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1466,6 +1466,7 @@ async def test_device_id(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, From fe1f617a354fdb41453363d8ae8f97e51d08f39d Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 10 Aug 2023 09:25:03 -0700 Subject: [PATCH 136/179] Add unifi power stats for PDU outlets (#98081) Adds support for power stats for PDU outlets. --- homeassistant/components/unifi/sensor.py | 31 ++++ tests/components/unifi/test_sensor.py | 176 +++++++++++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 23cc8724c2c..367ff1332f4 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -12,10 +12,12 @@ from typing import Generic from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients +from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client +from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port from aiounifi.models.wlan import Wlan @@ -84,6 +86,16 @@ def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int: ) +@callback +def async_device_outlet_power_supported_fn( + controller: UniFiController, obj_id: str +) -> bool: + """Determine if an outlet has the power property.""" + # At this time, an outlet_caps value of 3 is expected to indicate that the outlet + # supports metering + return controller.api.outlets[obj_id].caps == 3 + + @dataclass class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" @@ -193,6 +205,25 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda controller, obj_id: f"wlan_clients-{obj_id}", value_fn=async_wlan_client_value_fn, ), + UnifiSensorEntityDescription[Outlets, Outlet]( + key="Outlet power metering", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.outlets, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda outlet: f"{outlet.name} Outlet Power", + object_fn=lambda api, obj_id: api.outlets[obj_id], + should_poll=True, + supported_fn=async_device_outlet_power_supported_fn, + unique_id_fn=lambda controller, obj_id: f"outlet_power-{obj_id}", + value_fn=lambda _, obj: obj.power if obj.relay_state else "0", + ), ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 9670ecb43d0..98a4941caaa 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -132,6 +132,152 @@ WLAN = { "x_passphrase": "password", } +PDU_DEVICE_1 = { + "_id": "123456654321abcdef012345", + "required_version": "5.28.0", + "port_table": [], + "license_state": "registered", + "lcm_brightness_override": False, + "type": "usw", + "board_rev": 4, + "hw_caps": 136, + "reboot_duration": 70, + "snmp_contact": "", + "config_network": {"type": "dhcp", "bonding_enabled": False}, + "outlet_table": [ + { + "index": 1, + "relay_state": True, + "cycle_enabled": False, + "name": "USB Outlet 1", + "outlet_caps": 1, + }, + { + "index": 2, + "relay_state": True, + "cycle_enabled": False, + "name": "Outlet 2", + "outlet_caps": 3, + "outlet_voltage": "119.644", + "outlet_current": "0.935", + "outlet_power": "73.827", + "outlet_power_factor": "0.659", + }, + ], + "model": "USPPDUP", + "manufacturer_id": 4, + "ip": "192.168.1.76", + "fw2_caps": 0, + "jumboframe_enabled": False, + "version": "6.5.59.14777", + "unsupported_reason": 0, + "adoption_completed": True, + "outlet_enabled": True, + "stp_version": "rstp", + "name": "Dummy USP-PDU-Pro", + "fw_caps": 1732968229, + "lcm_brightness": 80, + "internet": True, + "mgmt_network_id": "123456654321abcdef012347", + "gateway_mac": "01:02:03:04:05:06", + "stp_priority": "32768", + "lcm_night_mode_begins": "22:00", + "two_phase_adopt": False, + "connected_at": 1690626493, + "inform_ip": "192.168.1.1", + "cfgversion": "ba8f30a5a17aad64", + "mac": "01:02:03:04:05:ff", + "provisioned_at": 1690989511, + "inform_url": "http://192.168.1.1:8080/inform", + "upgrade_duration": 100, + "ethernet_table": [{"num_port": 1, "name": "eth0", "mac": "01:02:03:04:05:a1"}], + "flowctrl_enabled": False, + "unsupported": False, + "ble_caps": 0, + "sys_error_caps": 0, + "dot1x_portctrl_enabled": False, + "last_uplink": {}, + "disconnected_at": 1690626452, + "architecture": "mips", + "x_aes_gcm": True, + "has_fan": False, + "outlet_overrides": [ + { + "cycle_enabled": False, + "name": "USB Outlet 1", + "relay_state": True, + "index": 1, + }, + {"cycle_enabled": False, "name": "Outlet 2", "relay_state": True, "index": 2}, + ], + "model_incompatible": False, + "satisfaction": 100, + "model_in_eol": False, + "anomalies": -1, + "has_temperature": False, + "switch_caps": {}, + "adopted_by_client": "web", + "snmp_location": "", + "model_in_lts": False, + "kernel_version": "4.14.115", + "serial": "abc123", + "power_source_ctrl_enabled": False, + "lcm_night_mode_ends": "08:00", + "adopted": True, + "hash_id": "abcdef123456", + "device_id": "mock-pdu", + "uplink": {}, + "state": 1, + "start_disconnected_millis": 1690626383386, + "credential_caps": 0, + "default": False, + "discovered_via": "l2", + "adopt_ip": "10.0.10.4", + "adopt_url": "http://192.168.1.1:8080/inform", + "last_seen": 1691518814, + "min_inform_interval_seconds": 10, + "upgradable": False, + "adoptable_when_upgraded": False, + "rollupgrade": False, + "known_cfgversion": "abcfde03929", + "uptime": 1193042, + "_uptime": 1193042, + "locating": False, + "start_connected_millis": 1690626493324, + "prev_non_busy_state": 5, + "next_interval": 47, + "sys_stats": {}, + "system-stats": {"cpu": "1.4", "mem": "28.9", "uptime": "1193042"}, + "ssh_session_table": [], + "lldp_table": [], + "displayable_version": "6.5.59", + "connection_network_id": "123456654321abcdef012349", + "connection_network_name": "Default", + "startup_timestamp": 1690325774, + "is_access_point": False, + "safe_for_autoupgrade": True, + "overheating": False, + "power_source": "0", + "total_max_power": 0, + "outlet_ac_power_budget": "1875.000", + "outlet_ac_power_consumption": "201.683", + "downlink_table": [], + "uplink_depth": 1, + "downlink_lldp_macs": [], + "dhcp_server_table": [], + "connect_request_ip": "10.0.10.4", + "connect_request_port": "57951", + "ipv4_lease_expiration_timestamp_seconds": 1691576686, + "stat": {}, + "tx_bytes": 1426780, + "rx_bytes": 1435064, + "bytes": 2861844, + "num_sta": 0, + "user-num_sta": 0, + "guest-num_sta": 0, + "x_has_ssh_hostkey": True, +} + async def test_no_clients( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -571,3 +717,33 @@ async def test_wlan_client_sensors( mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get("sensor.ssid_1").state == "0" + + +async def test_outlet_power_readings( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Test the outlet power reporting on PDU devices.""" + await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) + + assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power") + assert ent_reg_entry.unique_id == "outlet_power-01:02:03:04:05:ff_2" + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power") + assert outlet_2.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert outlet_2.state == "73.827" + + # Verify state update + pdu_device_state_update = deepcopy(PDU_DEVICE_1) + + pdu_device_state_update["outlet_table"][1]["outlet_power"] = "123.45" + + mock_unifi_websocket(message=MessageKey.DEVICE, data=pdu_device_state_update) + await hass.async_block_till_done() + + outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power") + assert outlet_2.state == "123.45" From 57d0fd7bb12a905309a02e68d8e9e3b1f10a6dc4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:25:28 +0200 Subject: [PATCH 137/179] Adjust derivative tests which create devices (#98186) --- tests/components/derivative/test_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 2d1d7a93afc..5ba00cabd9d 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -354,6 +354,7 @@ async def test_device_id(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, From e1e4b0dcf04e5ce3f3b33b28605c1f04df007df9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:25:42 +0200 Subject: [PATCH 138/179] Adjust device_automation tests which create devices (#98187) --- tests/components/device_automation/test_init.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index d0f013299b1..65fee1053ae 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1496,8 +1496,10 @@ async def test_automation_with_device_wrong_domain( module = module_cache["fake_integration.device_trigger"] module.async_validate_trigger_config = AsyncMock() + source_config_entry = MockConfigEntry(domain="not_fake_integration") + source_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( - config_entry_id="not_fake_integration_config_entry", + config_entry_id=source_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert await async_setup_component( From b11dc50f9e3494e29dd96222e2bf1311340db966 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:25:53 +0200 Subject: [PATCH 139/179] Adjust homekit_controller tests which create devices (#98194) --- tests/components/homekit_controller/test_connection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 5695077475f..e5949978215 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -97,10 +97,12 @@ async def test_migrate_device_id_no_serial_skip_if_other_owner( Create a device registry entry that needs migrate, but belongs to a different config entry. It should be ignored. """ + entry = MockConfigEntry() + entry.add_to_hass(hass) device_registry = dr.async_get(hass) bridge = device_registry.async_get_or_create( - config_entry_id="XX", + config_entry_id=entry.entry_id, identifiers=variant.before, manufacturer="RYSE Inc.", model="RYSE SmartBridge", @@ -115,7 +117,7 @@ async def test_migrate_device_id_no_serial_skip_if_other_owner( bridge = device_registry.async_get(bridge.id) assert bridge.identifiers == variant.before - assert bridge.config_entries == {"XX"} + assert bridge.config_entries == {entry.entry_id} @pytest.mark.parametrize("variant", DEVICE_MIGRATION_TESTS) From 6803a62368a4d88a60cf34ff2bdca72149c541da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:26:13 +0200 Subject: [PATCH 140/179] Adjust ruckus_unleashed tests which create devices (#98206) --- tests/components/ruckus_unleashed/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 5c50f845064..1e50ce7dec7 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -71,9 +71,11 @@ async def init_integration(hass) -> MockConfigEntry: entry = mock_config_entry() entry.add_to_hass(hass) # Make device tied to other integration so device tracker entities get enabled + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) dr.async_get(hass).async_get_or_create( name="Device from other integration", - config_entry_id=MockConfigEntry().entry_id, + config_entry_id=other_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, TEST_CLIENT[API_MAC])}, ) with patch( From fcdfeb74c87b14e2373cba42d88735291160377a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:26:20 +0200 Subject: [PATCH 141/179] Adjust smartthings tests which create devices (#98207) --- tests/components/smartthings/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index e3e80d80e52..a7a819d6ac9 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -37,7 +37,7 @@ from homeassistant.components.smartthings.const import ( STORAGE_VERSION, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_CLIENT_ID, @@ -55,13 +55,13 @@ COMPONENT_PREFIX = "homeassistant.components.smartthings." async def setup_platform(hass, platform: str, *, devices=None, scenes=None): """Set up the SmartThings platform and prerequisites.""" hass.config.components.add(DOMAIN) - config_entry = ConfigEntry( - 2, - DOMAIN, - "Test", - {CONF_INSTALLED_APP_ID: str(uuid4())}, - SOURCE_USER, + config_entry = MockConfigEntry( + version=2, + domain=DOMAIN, + title="Test", + data={CONF_INSTALLED_APP_ID: str(uuid4())}, ) + config_entry.add_to_hass(hass) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] ) From 3fdc98063e4e958e45a53a59e9f4c09d4add63b1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:26:44 +0200 Subject: [PATCH 142/179] Adjust bond tests which create devices (#98183) --- tests/components/bond/test_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 7dbd6696e18..33919219301 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -159,6 +159,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) + config_entry.add_to_hass(hass) old_identifers = (DOMAIN, "device_id") new_identifiers = (DOMAIN, "ZXXX12345", "device_id") @@ -170,8 +171,6 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant) -> None: name="old", ) - config_entry.add_to_hass(hass) - with patch_bond_bridge(), patch_bond_version( return_value={ "bondid": "ZXXX12345", From 49011f0158ba22118d4fd97c628245d154c3c555 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:27:05 +0200 Subject: [PATCH 143/179] Adjust hue tests which create devices (#98195) --- tests/components/hue/test_migration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index b03834c3249..ef51c2a2f89 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -48,6 +48,7 @@ async def test_light_entity_migration( ) -> None: """Test if entity schema for lights migrates from v1 to v2.""" config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + config_entry.add_to_hass(hass) ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) @@ -92,6 +93,7 @@ async def test_sensor_entity_migration( ) -> None: """Test if entity schema for sensors migrates from v1 to v2.""" config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + config_entry.add_to_hass(hass) ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) From f1d4a4bd26fdf32601ab1e729fb4292f6af450d5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 18:27:22 +0200 Subject: [PATCH 144/179] Adjust zwave_js tests which create devices (#98213) --- tests/components/zwave_js/test_helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index e38873322ae..b40c09b249d 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -9,12 +9,16 @@ from homeassistant.components.zwave_js.helpers import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import area_registry as ar, device_registry as dr +from tests.common import MockConfigEntry + async def test_async_get_node_status_sensor_entity_id(hass: HomeAssistant) -> None: """Test async_get_node_status_sensor_entity_id for non zwave_js device.""" dev_reg = dr.async_get(hass) + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) device = dev_reg.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry.entry_id, identifiers={("test", "test")}, ) assert async_get_node_status_sensor_entity_id(hass, device.id) is None From c7b4d4f3614230969000a92c7c36ce9e9c1d0562 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 19:28:16 +0200 Subject: [PATCH 145/179] Adjust helpers tests which create devices (#98214) --- tests/helpers/test_entity.py | 1 + tests/helpers/test_entity_platform.py | 25 +++++++++++++++++-------- tests/helpers/test_entity_registry.py | 7 +++++++ tests/helpers/test_intent.py | 8 +++++++- tests/helpers/test_template.py | 7 +++++++ 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 0d9ee76ac62..200b0230adb 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1360,6 +1360,7 @@ async def test_friendly_name_updated( platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 3eaad662d8b..77914a49894 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1062,8 +1062,10 @@ async def test_entity_registry_updates_invalid_entity_id(hass: HomeAssistant) -> async def test_device_info_called(hass: HomeAssistant) -> None: """Test device info is forwarded correctly.""" registry = dr.async_get(hass) + config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) via = registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry.entry_id, connections=set(), identifiers={("hue", "via-id")}, manufacturer="manufacturer", @@ -1098,7 +1100,6 @@ async def test_device_info_called(hass: HomeAssistant) -> None: return True platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1126,8 +1127,10 @@ async def test_device_info_called(hass: HomeAssistant) -> None: async def test_device_info_not_overrides(hass: HomeAssistant) -> None: """Test device info is forwarded correctly.""" registry = dr.async_get(hass) + config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) device = registry.async_get_or_create( - config_entry_id="bla", + config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "abcd")}, manufacturer="test-manufacturer", model="test-model", @@ -1154,7 +1157,6 @@ async def test_device_info_not_overrides(hass: HomeAssistant) -> None: return True platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1176,8 +1178,10 @@ async def test_device_info_homeassistant_url( ) -> None: """Test device info with homeassistant URL.""" registry = dr.async_get(hass) + config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry.entry_id, connections=set(), identifiers={("mqtt", "via-id")}, manufacturer="manufacturer", @@ -1201,7 +1205,6 @@ async def test_device_info_homeassistant_url( return True platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1222,8 +1225,10 @@ async def test_device_info_change_to_no_url( ) -> None: """Test device info changes to no URL.""" registry = dr.async_get(hass) + config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry.entry_id, connections=set(), identifiers={("mqtt", "via-id")}, manufacturer="manufacturer", @@ -1248,7 +1253,6 @@ async def test_device_info_change_to_no_url( return True platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1304,6 +1308,7 @@ async def test_entity_disabled_by_device(hass: HomeAssistant) -> None: platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id", domain=DOMAIN) + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1621,6 +1626,7 @@ async def test_entity_name_influences_entity_id( platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1690,6 +1696,7 @@ async def test_translated_entity_name_influences_entity_id( platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1773,6 +1780,7 @@ async def test_translated_device_class_name_influences_entity_id( platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1878,6 +1886,7 @@ async def test_device_type_error_checking( config_entry = MockConfigEntry( title="Mock Config Entry Title", entry_id="super-mock-id" ) + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 57622d330d9..f62addb9a64 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1017,6 +1017,7 @@ async def test_remove_device_removes_entities( ) -> None: """Test that we remove entities tied to a device.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1046,7 +1047,9 @@ async def test_remove_config_entry_from_device_removes_entities( ) -> None: """Test that we remove entities tied to a device when config entry is removed.""" config_entry_1 = MockConfigEntry(domain="hue") + config_entry_1.add_to_hass(hass) config_entry_2 = MockConfigEntry(domain="device_tracker") + config_entry_2.add_to_hass(hass) # Create device with two config entries device_registry.async_get_or_create( @@ -1112,7 +1115,9 @@ async def test_remove_config_entry_from_device_removes_entities_2( ) -> None: """Test that we don't remove entities with no config entry when device is modified.""" config_entry_1 = MockConfigEntry(domain="hue") + config_entry_1.add_to_hass(hass) config_entry_2 = MockConfigEntry(domain="device_tracker") + config_entry_2.add_to_hass(hass) # Create device with two config entries device_registry.async_get_or_create( @@ -1155,6 +1160,7 @@ async def test_update_device_race( ) -> None: """Test race when a device is created, updated and removed.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Create device device_entry = device_registry.async_get_or_create( @@ -1331,6 +1337,7 @@ async def test_disabled_entities_excluded_from_entity_list( ) -> None: """Test that disabled entities are excluded from async_entries_for_device.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 98e93785f58..8d473338058 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -18,6 +18,8 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + class MockIntentHandler(intent.IntentHandler): """Provide a mock intent handler.""" @@ -116,11 +118,15 @@ async def test_match_device_area( entity_registry: er.EntityRegistry, ) -> None: """Test async_match_state with a device in an area.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) area_kitchen = area_registry.async_get_or_create("kitchen") area_bedroom = area_registry.async_get_or_create("bedroom") kitchen_device = device_registry.async_get_or_create( - config_entry_id="1234", connections=set(), identifiers={("demo", "id-1234")} + config_entry_id=config_entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, ) device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 9994f0cadc1..d14496d321e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2709,6 +2709,7 @@ async def test_device_entities( ) -> None: """Test device_entities function.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Test non existing device ids info = render_to_info(hass, "{{ device_entities('abc123') }}") @@ -2858,6 +2859,7 @@ async def test_device_id( ) -> None: """Test device_id function.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(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")}, @@ -2903,6 +2905,7 @@ async def test_device_attr( ) -> None: """Test device_attr and is_device_attr functions.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Test non existing device ids (device_attr) info = render_to_info(hass, "{{ device_attr('abc123', 'id') }}") @@ -3049,6 +3052,7 @@ async def test_area_id( ) -> None: """Test area_id function.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Test non existing entity id info = render_to_info(hass, "{{ area_id('sensor.fake') }}") @@ -3155,6 +3159,7 @@ async def test_area_name( ) -> None: """Test area_name function.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Test non existing entity id info = render_to_info(hass, "{{ area_name('sensor.fake') }}") @@ -3236,6 +3241,7 @@ async def test_area_entities( ) -> None: """Test area_entities function.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Test non existing device id info = render_to_info(hass, "{{ area_entities('deadbeef') }}") @@ -3290,6 +3296,7 @@ async def test_area_devices( ) -> None: """Test area_devices function.""" config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) # Test non existing device id info = render_to_info(hass, "{{ area_devices('deadbeef') }}") From 4e8b81370e92b1049bca9624062f6aa1b66785f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Aug 2023 19:28:33 +0200 Subject: [PATCH 146/179] Adjust device_registry tests which create devices (#98215) --- tests/helpers/test_device_registry.py | 330 +++++++++++++++++--------- 1 file changed, 218 insertions(+), 112 deletions(-) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 9ebee025bd5..380574c04fa 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -34,15 +34,24 @@ def update_events(hass): return events +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create a mock config entry and add it to hass.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) + return entry + + async def test_get_or_create_returns_same_entry( hass: HomeAssistant, device_registry: dr.DeviceRegistry, area_registry: ar.AreaRegistry, + mock_config_entry: MockConfigEntry, update_events, ) -> None: """Make sure we do not duplicate entries.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, sw_version="sw-version", @@ -52,7 +61,7 @@ async def test_get_or_create_returns_same_entry( suggested_area="Game Room", ) entry2 = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "11:22:33:66:77:88")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -60,7 +69,7 @@ async def test_get_or_create_returns_same_entry( suggested_area="Game Room", ) entry3 = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) @@ -99,17 +108,18 @@ async def test_get_or_create_returns_same_entry( async def test_requirement_for_identifier_or_connection( device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, ) -> None: """Make sure we do require some descriptor of device.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers=set(), manufacturer="manufacturer", model="model", ) entry2 = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections=set(), identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -122,7 +132,7 @@ async def test_requirement_for_identifier_or_connection( with pytest.raises(HomeAssistantError): device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections=set(), identifiers=set(), manufacturer="manufacturer", @@ -130,24 +140,31 @@ async def test_requirement_for_identifier_or_connection( ) -async def test_multiple_config_entries(device_registry: dr.DeviceRegistry) -> None: +async def test_multiple_config_entries( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Make sure we do not get duplicate entries.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry2 = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry3 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -157,12 +174,14 @@ async def test_multiple_config_entries(device_registry: dr.DeviceRegistry) -> No assert len(device_registry.devices) == 1 assert entry.id == entry2.id assert entry.id == entry3.id - assert entry2.config_entries == {"123", "456"} + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} @pytest.mark.parametrize("load_registries", [False]) async def test_loading_from_storage( - hass: HomeAssistant, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, ) -> None: """Test loading stored devices on start.""" hass_storage[dr.STORAGE_KEY] = { @@ -172,7 +191,7 @@ async def test_loading_from_storage( "devices": [ { "area_id": "12345A", - "config_entries": ["1234"], + "config_entries": [mock_config_entry.entry_id], "configuration_url": "https://example.com/config", "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": dr.DeviceEntryDisabler.USER, @@ -190,7 +209,7 @@ async def test_loading_from_storage( ], "deleted_devices": [ { - "config_entries": ["1234"], + "config_entries": [mock_config_entry.entry_id], "connections": [["Zigbee", "23.45.67.89.01"]], "id": "bcdefghijklmn", "identifiers": [["serial", "34:56:AB:CD:EF:12"]], @@ -206,7 +225,7 @@ async def test_loading_from_storage( assert len(registry.deleted_devices) == 1 entry = registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "01.23.45.67.89")}, identifiers={("serial", "12:34:56:AB:CD:EF")}, manufacturer="manufacturer", @@ -214,7 +233,7 @@ async def test_loading_from_storage( ) assert entry == dr.DeviceEntry( area_id="12345A", - config_entries={"1234"}, + config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -235,14 +254,14 @@ async def test_loading_from_storage( # Restore a device, id should be reused from the deleted device entry entry = registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "23.45.67.89.01")}, identifiers={("serial", "34:56:AB:CD:EF:12")}, manufacturer="manufacturer", model="model", ) assert entry == dr.DeviceEntry( - config_entries={"1234"}, + config_entries={mock_config_entry.entry_id}, connections={("Zigbee", "23.45.67.89.01")}, id="bcdefghijklmn", identifiers={("serial", "34:56:AB:CD:EF:12")}, @@ -257,7 +276,9 @@ async def test_loading_from_storage( @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_1_to_1_3( - hass: HomeAssistant, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, ) -> None: """Test migration from version 1.1 to 1.3.""" hass_storage[dr.STORAGE_KEY] = { @@ -266,7 +287,7 @@ async def test_migration_1_1_to_1_3( "data": { "devices": [ { - "config_entries": ["1234"], + "config_entries": [mock_config_entry.entry_id], "connections": [["Zigbee", "01.23.45.67.89"]], "entry_type": "service", "id": "abcdefghijklm", @@ -310,7 +331,7 @@ async def test_migration_1_1_to_1_3( # Test data was loaded entry = registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "01.23.45.67.89")}, identifiers={("serial", "12:34:56:AB:CD:EF")}, ) @@ -318,7 +339,7 @@ async def test_migration_1_1_to_1_3( # Update to trigger a store entry = registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "01.23.45.67.89")}, identifiers={("serial", "12:34:56:AB:CD:EF")}, sw_version="new_version", @@ -335,7 +356,7 @@ async def test_migration_1_1_to_1_3( "devices": [ { "area_id": None, - "config_entries": ["1234"], + "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": None, @@ -383,7 +404,9 @@ async def test_migration_1_1_to_1_3( @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_2_to_1_3( - hass: HomeAssistant, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, ) -> None: """Test migration from version 1.2 to 1.3.""" hass_storage[dr.STORAGE_KEY] = { @@ -394,7 +417,7 @@ async def test_migration_1_2_to_1_3( "devices": [ { "area_id": None, - "config_entries": ["1234"], + "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": None, @@ -434,7 +457,7 @@ async def test_migration_1_2_to_1_3( # Test data was loaded entry = registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "01.23.45.67.89")}, identifiers={("serial", "12:34:56:AB:CD:EF")}, ) @@ -442,7 +465,7 @@ async def test_migration_1_2_to_1_3( # Update to trigger a store entry = registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "01.23.45.67.89")}, identifiers={("serial", "12:34:56:AB:CD:EF")}, sw_version="new_version", @@ -460,7 +483,7 @@ async def test_migration_1_2_to_1_3( "devices": [ { "area_id": None, - "config_entries": ["1234"], + "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": None, @@ -502,22 +525,27 @@ async def test_removing_config_entries( hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events ) -> None: """Make sure we do not get duplicate entries.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry2 = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry3 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, identifiers={("bridgeid", "4567")}, manufacturer="manufacturer", @@ -527,15 +555,15 @@ async def test_removing_config_entries( assert len(device_registry.devices) == 2 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == {"123", "456"} + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} - device_registry.async_clear_config_entry("123") + device_registry.async_clear_config_entry(config_entry_1.entry_id) entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) entry3_removed = device_registry.async_get_device( identifiers={("bridgeid", "4567")} ) - assert entry.config_entries == {"456"} + assert entry.config_entries == {config_entry_2.entry_id} assert entry3_removed is None await hass.async_block_till_done() @@ -546,13 +574,15 @@ async def test_removing_config_entries( assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id - assert update_events[1]["changes"] == {"config_entries": {"123"}} + assert update_events[1]["changes"] == {"config_entries": {config_entry_1.entry_id}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id assert "changes" not in update_events[2] assert update_events[3]["action"] == "update" assert update_events[3]["device_id"] == entry.id - assert update_events[3]["changes"] == {"config_entries": {"456", "123"}} + assert update_events[3]["changes"] == { + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} + } assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id assert "changes" not in update_events[4] @@ -562,22 +592,27 @@ async def test_deleted_device_removing_config_entries( hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events ) -> None: """Make sure we do not get duplicate entries.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry2 = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry3 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, identifiers={("bridgeid", "4567")}, manufacturer="manufacturer", @@ -588,7 +623,7 @@ async def test_deleted_device_removing_config_entries( assert len(device_registry.deleted_devices) == 0 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == {"123", "456"} + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} device_registry.async_remove_device(entry.id) device_registry.async_remove_device(entry3.id) @@ -603,7 +638,7 @@ async def test_deleted_device_removing_config_entries( assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id - assert update_events[1]["changes"] == {"config_entries": {"123"}} + assert update_events[1]["changes"] == {"config_entries": {config_entry_1.entry_id}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id assert "changes" not in update_events[2]["device_id"] @@ -614,11 +649,11 @@ async def test_deleted_device_removing_config_entries( assert update_events[4]["device_id"] == entry3.id assert "changes" not in update_events[4] - device_registry.async_clear_config_entry("123") + device_registry.async_clear_config_entry(config_entry_1.entry_id) assert len(device_registry.devices) == 0 assert len(device_registry.deleted_devices) == 2 - device_registry.async_clear_config_entry("456") + device_registry.async_clear_config_entry(config_entry_2.entry_id) assert len(device_registry.devices) == 0 assert len(device_registry.deleted_devices) == 2 @@ -628,7 +663,7 @@ async def test_deleted_device_removing_config_entries( # Re-add, expect to keep the device id entry2 = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -644,7 +679,7 @@ async def test_deleted_device_removing_config_entries( # Re-add, expect to get a new device id after the purge entry4 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -653,10 +688,12 @@ async def test_deleted_device_removing_config_entries( assert entry3.id != entry4.id -async def test_removing_area_id(device_registry: dr.DeviceRegistry) -> None: +async def test_removing_area_id( + device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry +) -> None: """Make sure we can clear area id.""" entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -672,10 +709,17 @@ async def test_removing_area_id(device_registry: dr.DeviceRegistry) -> None: assert entry_w_area != entry_wo_area -async def test_specifying_via_device_create(device_registry: dr.DeviceRegistry) -> None: +async def test_specifying_via_device_create( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test specifying a via_device and removal of the hub device.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + via = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("hue", "0123")}, manufacturer="manufacturer", @@ -683,7 +727,7 @@ async def test_specifying_via_device_create(device_registry: dr.DeviceRegistry) ) light = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections=set(), identifiers={("hue", "456")}, manufacturer="manufacturer", @@ -698,10 +742,17 @@ async def test_specifying_via_device_create(device_registry: dr.DeviceRegistry) assert light.via_device_id is None -async def test_specifying_via_device_update(device_registry: dr.DeviceRegistry) -> None: +async def test_specifying_via_device_update( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test specifying a via_device and updating.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + light = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections=set(), identifiers={("hue", "456")}, manufacturer="manufacturer", @@ -712,7 +763,7 @@ async def test_specifying_via_device_update(device_registry: dr.DeviceRegistry) assert light.via_device_id is None via = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("hue", "0123")}, manufacturer="manufacturer", @@ -720,7 +771,7 @@ async def test_specifying_via_device_update(device_registry: dr.DeviceRegistry) ) light = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections=set(), identifiers={("hue", "456")}, manufacturer="manufacturer", @@ -735,8 +786,19 @@ async def test_loading_saving_data( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test that we load/save data correctly.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + config_entry_3 = MockConfigEntry() + config_entry_3.add_to_hass(hass) + config_entry_4 = MockConfigEntry() + config_entry_4.add_to_hass(hass) + config_entry_5 = MockConfigEntry() + config_entry_5.add_to_hass(hass) + orig_via = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("hue", "0123")}, manufacturer="manufacturer", @@ -747,7 +809,7 @@ async def test_loading_saving_data( ) orig_light = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections=set(), identifiers={("hue", "456")}, manufacturer="manufacturer", @@ -757,7 +819,7 @@ async def test_loading_saving_data( ) orig_light2 = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections=set(), identifiers={("hue", "789")}, manufacturer="manufacturer", @@ -768,7 +830,7 @@ async def test_loading_saving_data( device_registry.async_remove_device(orig_light2.id) orig_light3 = device_registry.async_get_or_create( - config_entry_id="789", + config_entry_id=config_entry_3.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, identifiers={("hue", "abc")}, manufacturer="manufacturer", @@ -776,7 +838,7 @@ async def test_loading_saving_data( ) device_registry.async_get_or_create( - config_entry_id="abc", + config_entry_id=config_entry_4.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, identifiers={("abc", "123")}, manufacturer="manufacturer", @@ -786,7 +848,7 @@ async def test_loading_saving_data( device_registry.async_remove_device(orig_light3.id) orig_light4 = device_registry.async_get_or_create( - config_entry_id="789", + config_entry_id=config_entry_3.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, identifiers={("hue", "abc")}, manufacturer="manufacturer", @@ -797,7 +859,7 @@ async def test_loading_saving_data( assert orig_light4.id == orig_light3.id orig_kitchen_light = device_registry.async_get_or_create( - config_entry_id="999", + config_entry_id=config_entry_5.entry_id, connections=set(), identifiers={("hue", "999")}, manufacturer="manufacturer", @@ -851,10 +913,12 @@ async def test_loading_saving_data( assert orig_kitchen_light_witout_suggested_area == new_kitchen_light -async def test_no_unnecessary_changes(device_registry: dr.DeviceRegistry) -> None: +async def test_no_unnecessary_changes( + device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry +) -> None: """Make sure we do not consider devices changes.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("hue", "456"), ("bla", "123")}, ) @@ -862,22 +926,24 @@ async def test_no_unnecessary_changes(device_registry: dr.DeviceRegistry) -> Non "homeassistant.helpers.device_registry.DeviceRegistry.async_schedule_save" ) as mock_save: entry2 = device_registry.async_get_or_create( - config_entry_id="1234", identifiers={("hue", "456")} + config_entry_id=mock_config_entry.entry_id, identifiers={("hue", "456")} ) assert entry.id == entry2.id assert len(mock_save.mock_calls) == 0 -async def test_format_mac(device_registry: dr.DeviceRegistry) -> None: +async def test_format_mac( + device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry +) -> None: """Make sure we normalize mac addresses.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) for mac in ["123456ABCDEF", "123456abcdef", "12:34:56:ab:cd:ef", "1234.56ab.cdef"]: test_entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) assert test_entry.id == entry.id, mac @@ -895,18 +961,21 @@ async def test_format_mac(device_registry: dr.DeviceRegistry) -> None: "123.456.abc.def", # too many . ]: invalid_mac_entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, invalid)}, ) assert list(invalid_mac_entry.connections)[0][1] == invalid async def test_update( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + update_events, ) -> None: """Verify that we can update some attributes of a device.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("hue", "456"), ("bla", "123")}, ) @@ -936,7 +1005,7 @@ async def test_update( assert updated_entry != entry assert updated_entry == dr.DeviceEntry( area_id="12345A", - config_entries={"1234"}, + config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("mac", "12:34:56:ab:cd:ef")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -1001,22 +1070,27 @@ async def test_update_remove_config_entries( hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events ) -> None: """Make sure we do not get duplicate entries.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry2 = device_registry.async_get_or_create( - config_entry_id="456", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) entry3 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, identifiers={("bridgeid", "4567")}, manufacturer="manufacturer", @@ -1026,16 +1100,16 @@ async def test_update_remove_config_entries( assert len(device_registry.devices) == 2 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == {"123", "456"} + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} updated_entry = device_registry.async_update_device( - entry2.id, remove_config_entry_id="123" + entry2.id, remove_config_entry_id=config_entry_1.entry_id ) removed_entry = device_registry.async_update_device( - entry3.id, remove_config_entry_id="123" + entry3.id, remove_config_entry_id=config_entry_1.entry_id ) - assert updated_entry.config_entries == {"456"} + assert updated_entry.config_entries == {config_entry_2.entry_id} assert removed_entry is None removed_entry = device_registry.async_get_device(identifiers={("bridgeid", "4567")}) @@ -1050,13 +1124,15 @@ async def test_update_remove_config_entries( assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id - assert update_events[1]["changes"] == {"config_entries": {"123"}} + assert update_events[1]["changes"] == {"config_entries": {config_entry_1.entry_id}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id assert "changes" not in update_events[2] assert update_events[3]["action"] == "update" assert update_events[3]["device_id"] == entry.id - assert update_events[3]["changes"] == {"config_entries": {"456", "123"}} + assert update_events[3]["changes"] == { + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} + } assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id assert "changes" not in update_events[4] @@ -1066,11 +1142,12 @@ async def test_update_suggested_area( hass: HomeAssistant, device_registry: dr.DeviceRegistry, area_registry: ar.AreaRegistry, + mock_config_entry: MockConfigEntry, update_events, ) -> None: """Verify that we can update the suggested area version of a device.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bla", "123")}, ) @@ -1122,6 +1199,8 @@ async def test_cleanup_device_registry( """Test cleanup works.""" config_entry = MockConfigEntry(domain="hue") config_entry.add_to_hass(hass) + ghost_config_entry = MockConfigEntry() + ghost_config_entry.add_to_hass(hass) d1 = device_registry.async_get_or_create( identifiers={("hue", "d1")}, config_entry_id=config_entry.entry_id @@ -1133,14 +1212,17 @@ async def test_cleanup_device_registry( identifiers={("hue", "d3")}, config_entry_id=config_entry.entry_id ) device_registry.async_get_or_create( - identifiers={("something", "d4")}, config_entry_id="non_existing" + identifiers={("something", "d4")}, config_entry_id=ghost_config_entry.entry_id ) + # Remove the config entry without triggering the normal cleanup + hass.config_entries._entries.pop(ghost_config_entry.entry_id) ent_reg = er.async_get(hass) ent_reg.async_get_or_create("light", "hue", "e1", device_id=d1.id) ent_reg.async_get_or_create("light", "hue", "e2", device_id=d1.id) ent_reg.async_get_or_create("light", "hue", "e3", device_id=d3.id) + # Manual cleanup should detect the orphaned config entry dr.async_cleanup(hass, device_registry, ent_reg) assert device_registry.async_get_device(identifiers={("hue", "d1")}) is not None @@ -1233,11 +1315,14 @@ async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: async def test_restore_device( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + update_events, ) -> None: """Make sure device id is stable.""" entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -1253,14 +1338,14 @@ async def test_restore_device( assert len(device_registry.deleted_devices) == 1 entry2 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, identifiers={("bridgeid", "4567")}, manufacturer="manufacturer", model="model", ) entry3 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", @@ -1294,11 +1379,14 @@ async def test_restore_device( async def test_restore_simple_device( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + update_events, ) -> None: """Make sure device id is stable.""" entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, ) @@ -1312,12 +1400,12 @@ async def test_restore_simple_device( assert len(device_registry.deleted_devices) == 1 entry2 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, identifiers={("bridgeid", "4567")}, ) entry3 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, ) @@ -1348,8 +1436,13 @@ async def test_restore_shared_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, update_events ) -> None: """Make sure device id is stable for shared devices.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + entry = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("entry_123", "0123")}, manufacturer="manufacturer", @@ -1360,7 +1453,7 @@ async def test_restore_shared_device( assert len(device_registry.deleted_devices) == 0 device_registry.async_get_or_create( - config_entry_id="234", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("entry_234", "2345")}, manufacturer="manufacturer", @@ -1376,7 +1469,7 @@ async def test_restore_shared_device( assert len(device_registry.deleted_devices) == 1 entry2 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("entry_123", "0123")}, manufacturer="manufacturer", @@ -1394,7 +1487,7 @@ async def test_restore_shared_device( device_registry.async_remove_device(entry.id) entry3 = device_registry.async_get_or_create( - config_entry_id="234", + config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("entry_234", "2345")}, manufacturer="manufacturer", @@ -1410,7 +1503,7 @@ async def test_restore_shared_device( assert isinstance(entry3.identifiers, set) entry4 = device_registry.async_get_or_create( - config_entry_id="123", + config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("entry_123", "0123")}, manufacturer="manufacturer", @@ -1434,7 +1527,7 @@ async def test_restore_shared_device( assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id assert update_events[1]["changes"] == { - "config_entries": {"123"}, + "config_entries": {config_entry_1.entry_id}, "identifiers": {("entry_123", "0123")}, } assert update_events[2]["action"] == "remove" @@ -1452,17 +1545,18 @@ async def test_restore_shared_device( assert update_events[6]["action"] == "update" assert update_events[6]["device_id"] == entry.id assert update_events[6]["changes"] == { - "config_entries": {"234"}, + "config_entries": {config_entry_2.entry_id}, "identifiers": {("entry_234", "2345")}, } async def test_get_or_create_empty_then_set_default_values( device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, ) -> None: """Test creating an entry, then setting default name, model, manufacturer.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert entry.name is None @@ -1470,7 +1564,7 @@ async def test_get_or_create_empty_then_set_default_values( assert entry.manufacturer is None entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 1", default_model="default model 1", @@ -1481,7 +1575,7 @@ async def test_get_or_create_empty_then_set_default_values( assert entry.manufacturer == "default manufacturer 1" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 2", default_model="default model 2", @@ -1494,10 +1588,11 @@ async def test_get_or_create_empty_then_set_default_values( async def test_get_or_create_empty_then_update( device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, ) -> None: """Test creating an entry, then setting name, model, manufacturer.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert entry.name is None @@ -1505,7 +1600,7 @@ async def test_get_or_create_empty_then_update( assert entry.manufacturer is None entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, name="name 1", model="model 1", @@ -1516,7 +1611,7 @@ async def test_get_or_create_empty_then_update( assert entry.manufacturer == "manufacturer 1" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 1", default_model="default model 1", @@ -1529,10 +1624,11 @@ async def test_get_or_create_empty_then_update( async def test_get_or_create_sets_default_values( device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, ) -> None: """Test creating an entry, then setting default name, model, manufacturer.""" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 1", default_model="default model 1", @@ -1543,7 +1639,7 @@ async def test_get_or_create_sets_default_values( assert entry.manufacturer == "default manufacturer 1" entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 2", default_model="default model 2", @@ -1555,13 +1651,15 @@ async def test_get_or_create_sets_default_values( async def test_verify_suggested_area_does_not_overwrite_area_id( - device_registry: dr.DeviceRegistry, area_registry: ar.AreaRegistry + device_registry: dr.DeviceRegistry, + area_registry: ar.AreaRegistry, + mock_config_entry: MockConfigEntry, ) -> None: """Make sure suggested area does not override a set area id.""" game_room_area = area_registry.async_create("Game Room") original_entry = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, sw_version="sw-version", @@ -1576,7 +1674,7 @@ async def test_verify_suggested_area_does_not_overwrite_area_id( assert entry.area_id == game_room_area.id entry2 = device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, sw_version="sw-version", @@ -1713,16 +1811,21 @@ async def test_device_info_configuration_url_validation( expectation, ) -> None: """Test configuration URL of device info is properly validated.""" + config_entry_1 = MockConfigEntry() + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry() + config_entry_2.add_to_hass(hass) + with expectation: device_registry.async_get_or_create( - config_entry_id="1234", + config_entry_id=config_entry_1.entry_id, identifiers={("something", "1234")}, name="name", configuration_url=configuration_url, ) update_device = device_registry.async_get_or_create( - config_entry_id="5678", + config_entry_id=config_entry_2.entry_id, identifiers={("something", "5678")}, name="name", ) @@ -1734,7 +1837,9 @@ async def test_device_info_configuration_url_validation( @pytest.mark.parametrize("load_registries", [False]) async def test_loading_invalid_configuration_url_from_storage( - hass: HomeAssistant, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, ) -> None: """Test loading stored devices with an invalid URL.""" hass_storage[dr.STORAGE_KEY] = { @@ -1768,6 +1873,7 @@ async def test_loading_invalid_configuration_url_from_storage( registry = dr.async_get(hass) assert len(registry.devices) == 1 entry = registry.async_get_or_create( - config_entry_id="1234", identifiers={("serial", "12:34:56:AB:CD:EF")} + config_entry_id=mock_config_entry.entry_id, + identifiers={("serial", "12:34:56:AB:CD:EF")}, ) assert entry.configuration_url == "invalid" From 82ade574d8476c39d547f31b5faada3cc016e25b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 20:18:57 +0200 Subject: [PATCH 147/179] Migrate WAQI to aiowaqi library (#98000) * Migrate WAQI to aiowaqi library * Migrate WAQI to aiowaqi library * Migrate WAQI to aiowaqi library --- homeassistant/components/waqi/manifest.json | 2 +- homeassistant/components/waqi/sensor.py | 86 ++++++++------------- requirements_all.txt | 6 +- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index e5630d5fd29..2022558a500 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["waqiasync"], - "requirements": ["waqiasync==1.1.0"] + "requirements": ["aiowaqi==0.2.1"] } diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 71ec703df3f..51b9acb8e59 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -1,14 +1,11 @@ """Support for the World Air Quality Index service.""" from __future__ import annotations -import asyncio -from contextlib import suppress from datetime import timedelta import logging -import aiohttp +from aiowaqi import WAQIAirQuality, WAQIClient, WAQIConnectionError, WAQISearchResult import voluptuous as vol -from waqiasync import WaqiClient from homeassistant.components.sensor import ( SensorDeviceClass, @@ -39,17 +36,6 @@ ATTR_PM2_5 = "pm_2_5" ATTR_PRESSURE = "pressure" ATTR_SULFUR_DIOXIDE = "sulfur_dioxide" -KEY_TO_ATTR = { - "pm25": ATTR_PM2_5, - "pm10": ATTR_PM10, - "h": ATTR_HUMIDITY, - "p": ATTR_PRESSURE, - "t": ATTR_TEMPERATURE, - "o3": ATTR_OZONE, - "no2": ATTR_NITROGEN_DIOXIDE, - "so2": ATTR_SULFUR_DIOXIDE, -} - ATTRIBUTION = "Data provided by the World Air Quality Index project" ATTR_ICON = "mdi:cloud" @@ -82,7 +68,8 @@ async def async_setup_platform( station_filter = config.get(CONF_STATIONS) locations = config[CONF_LOCATIONS] - client = WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) + client = WAQIClient(session=async_get_clientsession(hass), request_timeout=TIMEOUT) + client.authenticate(token) dev = [] try: for location_name in locations: @@ -96,10 +83,7 @@ async def async_setup_platform( waqi_sensor.station_name, } & set(station_filter): dev.append(waqi_sensor) - except ( - aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, - ) as err: + except WAQIConnectionError as err: _LOGGER.exception("Failed to connect to WAQI servers") raise PlatformNotReady from err async_add_entities(dev, True) @@ -112,25 +96,14 @@ class WaqiSensor(SensorEntity): _attr_device_class = SensorDeviceClass.AQI _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, client, station): + _data: WAQIAirQuality | None = None + + def __init__(self, client: WAQIClient, search_result: WAQISearchResult) -> None: """Initialize the sensor.""" self._client = client - try: - self.uid = station["uid"] - except (KeyError, TypeError): - self.uid = None - - try: - self.url = station["station"]["url"] - except (KeyError, TypeError): - self.url = None - - try: - self.station_name = station["station"]["name"] - except (KeyError, TypeError): - self.station_name = None - - self._data = None + self.uid = search_result.station_id + self.url = search_result.station.external_url + self.station_name = search_result.station.name @property def name(self): @@ -140,12 +113,10 @@ class WaqiSensor(SensorEntity): return f"WAQI {self.url if self.url else self.uid}" @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the device.""" - if (value := self._data.get("aqi")) is not None: - with suppress(ValueError): - return float(value) - return None + assert self._data + return self._data.air_quality_index @property def available(self): @@ -166,28 +137,35 @@ class WaqiSensor(SensorEntity): try: attrs[ATTR_ATTRIBUTION] = " and ".join( [ATTRIBUTION] - + [v["name"] for v in self._data.get("attributions", [])] + + [attribution.name for attribution in self._data.attributions] ) - attrs[ATTR_TIME] = self._data["time"]["s"] - attrs[ATTR_DOMINENTPOL] = self._data.get("dominentpol") + attrs[ATTR_TIME] = self._data.measured_at + attrs[ATTR_DOMINENTPOL] = self._data.dominant_pollutant - iaqi = self._data["iaqi"] - for key in iaqi: - if key in KEY_TO_ATTR: - attrs[KEY_TO_ATTR[key]] = iaqi[key]["v"] - else: - attrs[key] = iaqi[key]["v"] - return attrs + iaqi = self._data.extended_air_quality + + attribute = { + ATTR_PM2_5: iaqi.pm25, + ATTR_PM10: iaqi.pm10, + ATTR_HUMIDITY: iaqi.humidity, + ATTR_PRESSURE: iaqi.pressure, + ATTR_TEMPERATURE: iaqi.temperature, + ATTR_OZONE: iaqi.ozone, + ATTR_NITROGEN_DIOXIDE: iaqi.nitrogen_dioxide, + ATTR_SULFUR_DIOXIDE: iaqi.sulfur_dioxide, + } + res_attributes = {k: v for k, v in attribute.items() if v is not None} + return {**attrs, **res_attributes} except (IndexError, KeyError): return {ATTR_ATTRIBUTION: ATTRIBUTION} async def async_update(self) -> None: """Get the latest data and updates the states.""" if self.uid: - result = await self._client.get_station_by_number(self.uid) + result = await self._client.get_by_station_number(self.uid) elif self.url: - result = await self._client.get_station_by_name(self.url) + result = await self._client.get_by_name(self.url) else: result = None self._data = result diff --git a/requirements_all.txt b/requirements_all.txt index bdd5ebce16f..7787e0e334f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -365,6 +365,9 @@ aiounifi==51 # homeassistant.components.vlc_telnet aiovlc==0.1.0 +# homeassistant.components.waqi +aiowaqi==0.2.1 + # homeassistant.components.watttime aiowatttime==0.1.1 @@ -2655,9 +2658,6 @@ wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.12 -# homeassistant.components.waqi -waqiasync==1.1.0 - # homeassistant.components.folder_watcher watchdog==2.3.1 From aacb8aecfc0e79d1954e4bcbbcdfe749fec97358 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 10 Aug 2023 21:46:56 +0200 Subject: [PATCH 148/179] Refactor Rest Sensor with ManualTriggerEntity (#97396) * ManualTriggerEntity for rest sensor * add availability test * review comments * last fixes --- homeassistant/components/rest/schema.py | 1 + homeassistant/components/rest/sensor.py | 68 +++++++++++++++++++----- homeassistant/helpers/template_entity.py | 14 +++++ tests/components/rest/test_sensor.py | 25 +++++++++ 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index c1f51286673..2f447b1c08c 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -76,6 +76,7 @@ SENSOR_SCHEMA = { vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_AVAILABILITY): cv.template, } BINARY_SENSOR_SCHEMA = { diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 18d0b6c7e76..1a74735c670 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -3,28 +3,40 @@ from __future__ import annotations import logging import ssl +from typing import Any from jsonpath import jsonpath import voluptuous as vol from homeassistant.components.sensor import ( + CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, + SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, + CONF_ICON, + CONF_NAME, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.template_entity import TemplateSensor +from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, + ManualTriggerSensorEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.json import json_loads @@ -43,6 +55,16 @@ PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA ) +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + async def async_setup_platform( hass: HomeAssistant, @@ -75,7 +97,14 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - unique_id: str | None = conf.get(CONF_UNIQUE_ID) + name = conf.get(CONF_NAME) or Template(DEFAULT_SENSOR_NAME, hass) + + trigger_entity_config = {CONF_NAME: name} + + for key in TRIGGER_ENTITY_OPTIONS: + if key not in conf: + continue + trigger_entity_config[key] = conf[key] async_add_entities( [ @@ -84,13 +113,13 @@ async def async_setup_platform( coordinator, rest, conf, - unique_id, + trigger_entity_config, ) ], ) -class RestSensor(RestEntity, TemplateSensor): +class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity): """Implementation of a REST sensor.""" def __init__( @@ -99,9 +128,10 @@ class RestSensor(RestEntity, TemplateSensor): coordinator: DataUpdateCoordinator[None] | None, rest: RestData, config: ConfigType, - unique_id: str | None, + trigger_entity_config: ConfigType, ) -> None: """Initialize the REST sensor.""" + ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config) RestEntity.__init__( self, coordinator, @@ -109,25 +139,30 @@ class RestSensor(RestEntity, TemplateSensor): config.get(CONF_RESOURCE_TEMPLATE), config[CONF_FORCE_UPDATE], ) - TemplateSensor.__init__( - self, - hass, - config=config, - fallback_name=DEFAULT_SENSOR_NAME, - unique_id=unique_id, - ) self._value_template = config.get(CONF_VALUE_TEMPLATE) if (value_template := self._value_template) is not None: value_template.hass = hass self._json_attrs = config.get(CONF_JSON_ATTRS) self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) + self._attr_extra_state_attributes = {} + + @property + def available(self) -> bool: + """Return if entity is available.""" + available1 = RestEntity.available.fget(self) # type: ignore[attr-defined] + available2 = ManualTriggerSensorEntity.available.fget(self) # type: ignore[attr-defined] + return bool(available1 and available2) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return extra attributes.""" + return dict(self._attr_extra_state_attributes) def _update_from_rest_data(self) -> None: """Update state from the rest data.""" value = self.rest.data_without_xml() if self._json_attrs: - self._attr_extra_state_attributes = {} if value: try: json_dict = json_loads(value) @@ -155,6 +190,8 @@ class RestSensor(RestEntity, TemplateSensor): else: _LOGGER.warning("Empty reply found when expecting JSON data") + raw_value = value + if value is not None and self._value_template is not None: value = self._value_template.async_render_with_possible_json_value( value, None @@ -165,8 +202,13 @@ class RestSensor(RestEntity, TemplateSensor): SensorDeviceClass.TIMESTAMP, ): self._attr_native_value = value + self._process_manual_data(raw_value) + self.async_write_ha_state() return self._attr_native_value = async_parse_date_datetime( value, self.entity_id, self.device_class ) + + self._process_manual_data(raw_value) + self.async_write_ha_state() diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 07dd154922c..07e68152d64 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -653,3 +653,17 @@ class ManualTriggerEntity(TriggerBaseEntity): variables = {"this": this, **(run_variables or {})} self._render_templates(variables) + + +class ManualTriggerSensorEntity(ManualTriggerEntity): + """Template entity based on manual trigger data for sensor.""" + + def __init__( + self, + hass: HomeAssistant, + config: dict, + ) -> None: + """Initialize the sensor entity.""" + ManualTriggerEntity.__init__(self, hass, config) + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_state_class = config.get(CONF_STATE_CLASS) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a7674937ab8..34e7233d33c 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, SERVICE_RELOAD, + STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfInformation, UnitOfTemperature, @@ -1018,3 +1019,27 @@ async def test_entity_config(hass: HomeAssistant) -> None: "state_class": "measurement", "unit_of_measurement": "°C", } + + +@respx.mock +async def test_availability_in_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # REST configuration + "platform": DOMAIN, + "method": "GET", + "resource": "http://localhost", + # Entity configuration + "availability": "{{value==1}}", + "name": "{{'REST' + ' ' + 'Sensor'}}", + }, + } + + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, text="123") + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.rest_sensor") + assert state.state == STATE_UNAVAILABLE From 86f94662eb2909cadaa658ba225914bfdee0af47 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 10 Aug 2023 23:49:14 +0200 Subject: [PATCH 149/179] Add entity translations to EZVIZ (#98123) --- homeassistant/components/ezviz/binary_sensor.py | 2 -- homeassistant/components/ezviz/button.py | 1 - homeassistant/components/ezviz/camera.py | 3 ++- homeassistant/components/ezviz/entity.py | 2 ++ homeassistant/components/ezviz/image.py | 2 -- homeassistant/components/ezviz/light.py | 3 +-- homeassistant/components/ezviz/number.py | 2 +- homeassistant/components/ezviz/select.py | 2 -- homeassistant/components/ezviz/sensor.py | 2 -- homeassistant/components/ezviz/strings.json | 15 +++++++++++++++ homeassistant/components/ezviz/switch.py | 2 -- homeassistant/components/ezviz/update.py | 3 +-- 12 files changed, 22 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 3ed61d8fc3d..81697e2772c 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -54,8 +54,6 @@ async def async_setup_entry( class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """Representation of a EZVIZ sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/button.py b/homeassistant/components/ezviz/button.py index 1c04de956c6..2199f82a476 100644 --- a/homeassistant/components/ezviz/button.py +++ b/homeassistant/components/ezviz/button.py @@ -103,7 +103,6 @@ class EzvizButtonEntity(EzvizEntity, ButtonEntity): """Representation of a EZVIZ button entity.""" entity_description: EzvizButtonEntityDescription - _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 7f03aef1d97..083e433952f 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -170,6 +170,8 @@ async def async_setup_entry( class EzvizCamera(EzvizEntity, Camera): """An implementation of a EZVIZ security camera.""" + _attr_name = None + def __init__( self, hass: HomeAssistant, @@ -192,7 +194,6 @@ class EzvizCamera(EzvizEntity, Camera): self._ffmpeg_arguments = ffmpeg_arguments self._ffmpeg = get_ffmpeg_manager(hass) self._attr_unique_id = serial - self._attr_name = self.data["name"] if camera_password: self._attr_supported_features = CameraEntityFeature.STREAM diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index ccf273a970b..d3720170c29 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -14,6 +14,8 @@ from .coordinator import EzvizDataUpdateCoordinator class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): """Generic entity encapsulating common features of EZVIZ device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py index 3de4f55a9d4..aeb8eafe68f 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -38,8 +38,6 @@ async def async_setup_entry( class EzvizLastMotion(EzvizEntity, ImageEntity): """Return Last Motion Image from Ezviz Camera.""" - _attr_has_entity_name = True - def __init__( self, hass: HomeAssistant, coordinator: EzvizDataUpdateCoordinator, serial: str ) -> None: diff --git a/homeassistant/components/ezviz/light.py b/homeassistant/components/ezviz/light.py index 9702959649d..558072658d3 100644 --- a/homeassistant/components/ezviz/light.py +++ b/homeassistant/components/ezviz/light.py @@ -44,7 +44,7 @@ async def async_setup_entry( class EzvizLight(EzvizEntity, LightEntity): """Representation of a EZVIZ light.""" - _attr_has_entity_name = True + _attr_translation_key = "light" _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @@ -60,7 +60,6 @@ class EzvizLight(EzvizEntity, LightEntity): == DeviceCatagories.BATTERY_CAMERA_DEVICE_CATEGORY.value ) self._attr_unique_id = f"{serial}_Light" - self._attr_name = "Light" self._attr_is_on = self.data["switches"][DeviceSwitchType.ALARM_LIGHT.value] self._attr_brightness = round( percentage_to_ranged_value( diff --git a/homeassistant/components/ezviz/number.py b/homeassistant/components/ezviz/number.py index 74d496ef6c1..e4d39894d85 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -47,7 +47,7 @@ class EzvizNumberEntityDescription( NUMBER_TYPE = EzvizNumberEntityDescription( key="detection_sensibility", - name="Detection sensitivity", + translation_key="detection_sensibility", icon="mdi:eye", entity_category=EntityCategory.CONFIG, native_min_value=0, diff --git a/homeassistant/components/ezviz/select.py b/homeassistant/components/ezviz/select.py index ef1dd785392..369a429dbe6 100644 --- a/homeassistant/components/ezviz/select.py +++ b/homeassistant/components/ezviz/select.py @@ -63,8 +63,6 @@ async def async_setup_entry( class EzvizSelect(EzvizEntity, SelectEntity): """Representation of a EZVIZ select entity.""" - _attr_has_entity_name = True - def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 9b19148bdb7..aecf25c2c78 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -92,8 +92,6 @@ async def async_setup_entry( class EzvizSensor(EzvizEntity, SensorEntity): """Representation of a EZVIZ sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: EzvizDataUpdateCoordinator, serial: str, sensor: str ) -> None: diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 590f95029c6..3e8797e7c02 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -132,6 +132,16 @@ "name": "Encryption" } }, + "light": { + "light": { + "name": "[%key:component::light::title%]" + } + }, + "number": { + "detection_sensibility": { + "name": "Detection sensitivity" + } + }, "sensor": { "alarm_sound_mod": { "name": "Alarm sound level" @@ -201,6 +211,11 @@ "follow_movement": { "name": "Follow movement" } + }, + "update": { + "firmware": { + "name": "[%key:component::update::entity_component::firmware::name%]" + } } }, "services": { diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 337a7080506..4089b0ae393 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -134,8 +134,6 @@ async def async_setup_entry( class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a EZVIZ sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: EzvizDataUpdateCoordinator, serial: str, switch_number: int ) -> None: diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py index 3acc1032514..6a80a579080 100644 --- a/homeassistant/components/ezviz/update.py +++ b/homeassistant/components/ezviz/update.py @@ -24,7 +24,7 @@ PARALLEL_UPDATES = 1 UPDATE_ENTITY_TYPES = UpdateEntityDescription( key="version", - name="Firmware update", + translation_key="firmware", device_class=UpdateDeviceClass.FIRMWARE, ) @@ -49,7 +49,6 @@ async def async_setup_entry( class EzvizUpdateEntity(EzvizEntity, UpdateEntity): """Representation of a EZVIZ Update entity.""" - _attr_has_entity_name = True _attr_supported_features = ( UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS From b653d7f68350ec04eef7f9987714c3b1e4d4addd Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 10 Aug 2023 18:00:54 -0400 Subject: [PATCH 150/179] Fix Enphase dry contact binary sensor state updates (#98225) Fix dry contact binary sensor state updates --- .../components/enphase_envoy/binary_sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 42778aff9d6..a5ebb476c59 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -212,15 +212,15 @@ class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): self, coordinator: EnphaseUpdateCoordinator, description: BinarySensorEntityDescription, - relay: str, + relay_id: str, ) -> None: """Init the Enpower base entity.""" super().__init__(coordinator, description) enpower = self.data.enpower assert enpower is not None - self.relay = self.data.dry_contact_status[relay] + self.relay_id = relay_id self._serial_number = enpower.serial_number - self._attr_unique_id = f"{self._serial_number}_relay_{self.relay.id}" + self._attr_unique_id = f"{self._serial_number}_relay_{relay_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._serial_number)}, manufacturer="Enphase", @@ -229,11 +229,10 @@ class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): sw_version=str(enpower.firmware_version), via_device=(DOMAIN, self.envoy_serial_num), ) - self._attr_name = ( - f"{self.data.dry_contact_settings[self.relay.id].load_name} Relay" - ) + self._attr_name = f"{self.data.dry_contact_settings[relay_id].load_name} Relay" @property def is_on(self) -> bool: """Return the state of the Enpower binary_sensor.""" - return self.relay.status == DryContactStatus.CLOSED + relay = self.data.dry_contact_status[self.relay_id] + return relay.status == DryContactStatus.CLOSED From 296c27859eb823cd711f53a5d65d46becf0e6ee8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Aug 2023 03:57:42 +0200 Subject: [PATCH 151/179] Fix issue registry sending unneeded update events (#98230) --- homeassistant/helpers/issue_registry.py | 14 ++++++--- tests/helpers/test_issue_registry.py | 42 ++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 9bd6ebffadb..30866ccf7cd 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -154,7 +154,7 @@ class IssueRegistry: {"action": "create", "domain": domain, "issue_id": issue_id}, ) else: - issue = self.issues[(domain, issue_id)] = dataclasses.replace( + replacement = dataclasses.replace( issue, active=True, breaks_in_ha_version=breaks_in_ha_version, @@ -167,10 +167,14 @@ class IssueRegistry: translation_key=translation_key, translation_placeholders=translation_placeholders, ) - self.hass.bus.async_fire( - EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, - {"action": "update", "domain": domain, "issue_id": issue_id}, - ) + # Only fire is something changed + if replacement != issue: + issue = self.issues[(domain, issue_id)] = replacement + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) return issue diff --git a/tests/helpers/test_issue_registry.py b/tests/helpers/test_issue_registry.py index 51cffbc7810..d184ccf0a2b 100644 --- a/tests/helpers/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -109,11 +109,51 @@ async def test_load_issues(hass: HomeAssistant) -> None: "issue_id": "issue_1", } - ir.async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + # Update an issue by creating it again with the same value, + # no update event should be fired, as nothing changed. + ir.async_create_issue( + hass, + issues[2]["domain"], + issues[2]["issue_id"], + breaks_in_ha_version=issues[2]["breaks_in_ha_version"], + is_fixable=issues[2]["is_fixable"], + is_persistent=issues[2]["is_persistent"], + learn_more_url=issues[2]["learn_more_url"], + severity=issues[2]["severity"], + translation_key=issues[2]["translation_key"], + translation_placeholders=issues[2]["translation_placeholders"], + ) + await hass.async_block_till_done() + + assert len(events) == 5 + + # Update an issue by creating it again, url changed + ir.async_create_issue( + hass, + issues[2]["domain"], + issues[2]["issue_id"], + breaks_in_ha_version=issues[2]["breaks_in_ha_version"], + is_fixable=issues[2]["is_fixable"], + is_persistent=issues[2]["is_persistent"], + learn_more_url="https://www.example.com/something_changed", + severity=issues[2]["severity"], + translation_key=issues[2]["translation_key"], + translation_placeholders=issues[2]["translation_placeholders"], + ) await hass.async_block_till_done() assert len(events) == 6 assert events[5].data == { + "action": "update", + "domain": "test", + "issue_id": "issue_3", + } + + ir.async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + await hass.async_block_till_done() + + assert len(events) == 7 + assert events[6].data == { "action": "remove", "domain": "test", "issue_id": "issue_3", From 108bcabf75cf9e68b76de70f281842be760d6928 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Aug 2023 15:59:37 -1000 Subject: [PATCH 152/179] Add missing transmit power to ESPHome Bluetooth scanners (#98175) We did not previously have a way to get the transmit power value when using ESPHome scanners. bluetooth-data-tools 1.8.0 includes it in the advertisment tuple to fully align with the bleak implementation. txpower is not yet used in the HA codebase but may be expected by upstream libaries that calculate estimated distance --- homeassistant/components/bluetooth/manifest.json | 2 +- .../components/esphome/bluetooth/scanner.py | 14 +++++++------- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 481a760ba88..b1281af2bc2 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.1.1", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", - "bluetooth-data-tools==1.7.0", + "bluetooth-data-tools==1.8.0", "dbus-fast==1.91.2" ] } diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 5013a288dcf..a54e7af59a6 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -2,7 +2,10 @@ from __future__ import annotations from aioesphomeapi import BluetoothLEAdvertisement, BluetoothLERawAdvertisement -from bluetooth_data_tools import int_to_bluetooth_address, parse_advertisement_data +from bluetooth_data_tools import ( + int_to_bluetooth_address, + parse_advertisement_data_tuple, +) from homeassistant.components.bluetooth import MONOTONIC_TIME, BaseHaRemoteScanner from homeassistant.core import callback @@ -11,6 +14,8 @@ from homeassistant.core import callback class ESPHomeScanner(BaseHaRemoteScanner): """Scanner for esphome.""" + __slots__ = () + @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" @@ -34,15 +39,10 @@ class ESPHomeScanner(BaseHaRemoteScanner): """Call the registered callback.""" now = MONOTONIC_TIME() for adv in advertisements: - parsed = parse_advertisement_data((adv.data,)) self._async_on_advertisement( int_to_bluetooth_address(adv.address), adv.rssi, - parsed.local_name, - parsed.service_uuids, - parsed.service_data, - parsed.manufacturer_data, - None, + *parse_advertisement_data_tuple((adv.data,)), {"address_type": adv.address_type}, now, ) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 303e773bbd3..c44c8b3e28d 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async_interrupt==1.1.1", "aioesphomeapi==15.1.15", - "bluetooth-data-tools==1.7.0", + "bluetooth-data-tools==1.8.0", "esphome-dashboard-api==1.2.3" ], "zeroconf": ["_esphomelib._tcp.local."] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 60d2efe6536..1115a0efc54 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.7.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.8.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 9ebbe07703a..ffaf2bf87db 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -32,5 +32,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.7.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.8.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f3cfab069f0..282ff1ddb44 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ bleak-retry-connector==3.1.1 bleak==0.20.2 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 -bluetooth-data-tools==1.7.0 +bluetooth-data-tools==1.8.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 7787e0e334f..6b88719a737 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.7.0 +bluetooth-data-tools==1.8.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 341c2371c7a..ddb0cf0f8f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.7.0 +bluetooth-data-tools==1.8.0 # homeassistant.components.bond bond-async==0.2.1 From 045c3279280e542ff1f082af166ab8949dd11a17 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Aug 2023 04:04:26 +0200 Subject: [PATCH 153/179] Move DeviceInfo from entity to device registry (#98149) * Move DeviceInfo from entity to device registry * Update integrations --- homeassistant/components/abode/__init__.py | 5 ++-- .../components/accuweather/__init__.py | 3 +-- homeassistant/components/acmeda/base.py | 4 ++-- homeassistant/components/adax/climate.py | 2 +- homeassistant/components/adguard/entity.py | 4 ++-- .../components/advantage_air/entity.py | 2 +- .../components/advantage_air/light.py | 2 +- .../components/advantage_air/update.py | 2 +- .../agent_dvr/alarm_control_panel.py | 2 +- homeassistant/components/agent_dvr/camera.py | 2 +- homeassistant/components/airly/sensor.py | 3 +-- homeassistant/components/airnow/sensor.py | 3 +-- homeassistant/components/airq/coordinator.py | 2 +- homeassistant/components/airthings/sensor.py | 2 +- .../components/airthings_ble/sensor.py | 3 +-- homeassistant/components/airtouch4/climate.py | 2 +- .../components/airvisual_pro/__init__.py | 3 ++- homeassistant/components/airzone/entity.py | 2 +- .../components/airzone_cloud/entity.py | 2 +- .../components/aladdin_connect/cover.py | 2 +- .../components/aladdin_connect/sensor.py | 2 +- .../components/ambiclimate/climate.py | 2 +- .../components/ambient_station/__init__.py | 3 ++- .../components/android_ip_webcam/camera.py | 2 +- .../components/android_ip_webcam/entity.py | 2 +- .../components/androidtv/media_player.py | 3 +-- .../components/androidtv_remote/entity.py | 4 ++-- homeassistant/components/anova/coordinator.py | 2 +- .../components/anthemav/media_player.py | 2 +- homeassistant/components/apcupsd/__init__.py | 2 +- homeassistant/components/apple_tv/__init__.py | 3 ++- homeassistant/components/aranet/sensor.py | 2 +- .../components/arcam_fmj/media_player.py | 2 +- .../components/aseko_pool_live/entity.py | 2 +- homeassistant/components/asuswrt/router.py | 3 +-- homeassistant/components/atag/__init__.py | 2 +- homeassistant/components/aten_pe/switch.py | 3 +-- homeassistant/components/august/entity.py | 3 ++- homeassistant/components/aurora/entity.py | 3 +-- .../aurora_abb_powerone/aurora_device.py | 3 ++- .../components/aussie_broadband/sensor.py | 3 +-- homeassistant/components/awair/sensor.py | 2 +- homeassistant/components/axis/entity.py | 3 ++- .../components/azure_devops/__init__.py | 4 ++-- homeassistant/components/baf/entity.py | 4 ++-- homeassistant/components/balboa/entity.py | 4 ++-- homeassistant/components/blebox/__init__.py | 3 ++- .../components/blink/alarm_control_panel.py | 2 +- .../components/blink/binary_sensor.py | 2 +- homeassistant/components/blink/camera.py | 2 +- homeassistant/components/blink/sensor.py | 2 +- .../bluetooth/passive_update_processor.py | 3 ++- .../bmw_connected_drive/__init__.py | 2 +- homeassistant/components/bond/entity.py | 3 ++- homeassistant/components/bosch_shc/entity.py | 4 ++-- homeassistant/components/braviatv/entity.py | 2 +- homeassistant/components/broadlink/entity.py | 3 ++- homeassistant/components/brother/sensor.py | 2 +- .../components/brottsplatskartan/sensor.py | 3 +-- homeassistant/components/brunt/cover.py | 2 +- homeassistant/components/bsblan/entity.py | 4 ++-- homeassistant/components/canary/camera.py | 2 +- homeassistant/components/canary/sensor.py | 2 +- homeassistant/components/cast/media_player.py | 2 +- .../components/cert_expiry/sensor.py | 3 +-- homeassistant/components/co2signal/sensor.py | 3 +-- homeassistant/components/coinbase/sensor.py | 3 +-- homeassistant/components/control4/__init__.py | 2 +- homeassistant/components/coolmaster/entity.py | 2 +- homeassistant/components/cpuspeed/sensor.py | 2 +- .../components/crownstone/devices.py | 3 ++- homeassistant/components/daikin/__init__.py | 3 +-- homeassistant/components/daikin/sensor.py | 2 +- homeassistant/components/daikin/switch.py | 2 +- .../components/deconz/deconz_device.py | 4 ++-- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deluge/__init__.py | 3 +-- .../components/demo/binary_sensor.py | 2 +- homeassistant/components/demo/button.py | 2 +- homeassistant/components/demo/climate.py | 2 +- homeassistant/components/demo/cover.py | 2 +- homeassistant/components/demo/date.py | 2 +- homeassistant/components/demo/datetime.py | 2 +- homeassistant/components/demo/event.py | 2 +- homeassistant/components/demo/light.py | 2 +- homeassistant/components/demo/number.py | 2 +- homeassistant/components/demo/select.py | 2 +- homeassistant/components/demo/sensor.py | 2 +- homeassistant/components/demo/switch.py | 2 +- homeassistant/components/demo/text.py | 2 +- homeassistant/components/demo/time.py | 2 +- homeassistant/components/demo/update.py | 2 +- .../components/denonavr/media_player.py | 2 +- homeassistant/components/derivative/sensor.py | 2 +- .../components/device_tracker/config_entry.py | 3 ++- .../devolo_home_control/devolo_device.py | 3 ++- .../components/devolo_home_network/entity.py | 3 ++- homeassistant/components/directv/entity.py | 3 ++- homeassistant/components/discovergy/sensor.py | 2 +- homeassistant/components/dlink/entity.py | 3 ++- homeassistant/components/dnsip/sensor.py | 3 +-- homeassistant/components/doorbird/entity.py | 3 ++- .../components/dormakaba_dkey/entity.py | 2 +- .../components/dremel_3d_printer/entity.py | 3 ++- homeassistant/components/dsmr/sensor.py | 2 +- .../components/dunehd/media_player.py | 2 +- homeassistant/components/duotecno/entity.py | 3 ++- .../components/dynalite/dynalitebase.py | 2 +- homeassistant/components/eafm/sensor.py | 3 +-- homeassistant/components/easyenergy/sensor.py | 3 +-- .../components/ecobee/binary_sensor.py | 2 +- homeassistant/components/ecobee/climate.py | 2 +- homeassistant/components/ecobee/entity.py | 3 ++- homeassistant/components/ecobee/humidifier.py | 2 +- homeassistant/components/ecobee/sensor.py | 2 +- homeassistant/components/ecobee/weather.py | 2 +- homeassistant/components/econet/__init__.py | 3 ++- homeassistant/components/ecowitt/entity.py | 3 ++- homeassistant/components/edl21/sensor.py | 2 +- homeassistant/components/efergy/__init__.py | 3 ++- .../components/eight_sleep/__init__.py | 3 +-- .../components/electrasmart/climate.py | 2 +- homeassistant/components/elgato/entity.py | 7 ++++-- homeassistant/components/elkm1/__init__.py | 4 ++-- homeassistant/components/elmax/common.py | 2 +- homeassistant/components/emonitor/sensor.py | 2 +- homeassistant/components/energyzero/sensor.py | 3 +-- .../components/enphase_envoy/binary_sensor.py | 2 +- .../components/enphase_envoy/sensor.py | 3 ++- .../components/environment_canada/__init__.py | 3 +-- .../components/epson/media_player.py | 2 +- homeassistant/components/escea/climate.py | 2 +- homeassistant/components/esphome/entity.py | 3 ++- homeassistant/components/esphome/update.py | 2 +- .../components/eufylife_ble/sensor.py | 2 +- .../components/evil_genius_labs/__init__.py | 2 +- .../components/ezviz/alarm_control_panel.py | 2 +- homeassistant/components/ezviz/entity.py | 4 ++-- homeassistant/components/fibaro/__init__.py | 3 ++- homeassistant/components/fibaro/scene.py | 2 +- homeassistant/components/filesize/sensor.py | 3 +-- homeassistant/components/firmata/entity.py | 2 +- homeassistant/components/fivem/entity.py | 3 ++- .../components/fjaraskupan/__init__.py | 3 ++- .../components/fjaraskupan/binary_sensor.py | 3 ++- .../components/fjaraskupan/coordinator.py | 2 +- homeassistant/components/fjaraskupan/fan.py | 2 +- homeassistant/components/fjaraskupan/light.py | 3 ++- .../components/fjaraskupan/number.py | 3 ++- .../components/fjaraskupan/sensor.py | 3 ++- homeassistant/components/flipr/__init__.py | 3 ++- homeassistant/components/flo/entity.py | 4 ++-- homeassistant/components/flume/entity.py | 3 ++- homeassistant/components/flux_led/entity.py | 3 ++- .../components/forecast_solar/sensor.py | 3 +-- homeassistant/components/freebox/button.py | 2 +- homeassistant/components/freebox/home_base.py | 3 ++- homeassistant/components/freebox/router.py | 3 +-- homeassistant/components/freebox/sensor.py | 2 +- .../components/freedompro/binary_sensor.py | 2 +- .../components/freedompro/climate.py | 2 +- homeassistant/components/freedompro/cover.py | 2 +- homeassistant/components/freedompro/fan.py | 2 +- homeassistant/components/freedompro/light.py | 2 +- homeassistant/components/freedompro/lock.py | 2 +- homeassistant/components/freedompro/sensor.py | 2 +- homeassistant/components/freedompro/switch.py | 2 +- homeassistant/components/fritz/button.py | 3 +-- homeassistant/components/fritz/common.py | 3 ++- homeassistant/components/fritz/switch.py | 4 ++-- homeassistant/components/fritzbox/__init__.py | 3 ++- homeassistant/components/fritzbox/button.py | 2 +- .../components/fritzbox_callmonitor/sensor.py | 2 +- homeassistant/components/fronius/__init__.py | 2 +- homeassistant/components/fronius/const.py | 2 +- homeassistant/components/fronius/sensor.py | 2 +- .../frontier_silicon/media_player.py | 2 +- .../components/fully_kiosk/entity.py | 4 ++-- .../components/gardena_bluetooth/__init__.py | 2 +- .../gardena_bluetooth/coordinator.py | 3 ++- homeassistant/components/geocaching/sensor.py | 3 +-- .../components/geofency/device_tracker.py | 2 +- homeassistant/components/gios/sensor.py | 3 +-- homeassistant/components/github/sensor.py | 3 +-- homeassistant/components/glances/sensor.py | 2 +- homeassistant/components/goalzero/entity.py | 3 ++- homeassistant/components/gogogate2/common.py | 2 +- homeassistant/components/goodwe/__init__.py | 2 +- homeassistant/components/goodwe/button.py | 2 +- homeassistant/components/goodwe/number.py | 2 +- homeassistant/components/goodwe/select.py | 2 +- homeassistant/components/goodwe/sensor.py | 2 +- .../components/google_assistant/button.py | 2 +- .../components/google_mail/entity.py | 4 ++-- .../components/google_travel_time/sensor.py | 3 +-- .../components/gpslogger/device_tracker.py | 2 +- homeassistant/components/gree/climate.py | 3 +-- homeassistant/components/gree/entity.py | 3 +-- .../components/growatt_server/sensor.py | 2 +- homeassistant/components/guardian/__init__.py | 3 ++- homeassistant/components/harmony/data.py | 2 +- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/hassio/entity.py | 3 ++- homeassistant/components/heos/media_player.py | 2 +- .../components/here_travel_time/sensor.py | 3 +-- homeassistant/components/hive/__init__.py | 4 ++-- .../components/home_connect/entity.py | 3 ++- .../components/home_plus_control/switch.py | 2 +- .../homekit_controller/connection.py | 2 +- .../components/homekit_controller/entity.py | 3 ++- .../homematicip_cloud/alarm_control_panel.py | 2 +- .../homematicip_cloud/binary_sensor.py | 2 +- .../components/homematicip_cloud/climate.py | 2 +- .../homematicip_cloud/generic_entity.py | 3 ++- homeassistant/components/homewizard/entity.py | 2 +- homeassistant/components/honeywell/climate.py | 2 +- homeassistant/components/honeywell/sensor.py | 2 +- .../components/huawei_lte/__init__.py | 3 ++- homeassistant/components/hue/scene.py | 3 +-- homeassistant/components/hue/v1/light.py | 2 +- .../components/hue/v1/sensor_device.py | 5 ++-- homeassistant/components/hue/v2/entity.py | 7 ++++-- homeassistant/components/hue/v2/group.py | 3 +-- .../hunterdouglas_powerview/entity.py | 2 +- .../hvv_departures/binary_sensor.py | 3 +-- .../components/hvv_departures/sensor.py | 3 +-- homeassistant/components/hyperion/camera.py | 2 +- homeassistant/components/hyperion/light.py | 2 +- homeassistant/components/hyperion/switch.py | 2 +- .../components/ialarm/alarm_control_panel.py | 2 +- .../components/iaqualink/__init__.py | 3 ++- homeassistant/components/ibeacon/entity.py | 3 ++- .../components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- homeassistant/components/imap/sensor.py | 3 +-- .../components/insteon/insteon_entity.py | 3 ++- .../components/integration/sensor.py | 2 +- .../components/intellifire/coordinator.py | 2 +- homeassistant/components/ios/sensor.py | 2 +- homeassistant/components/iotawatt/sensor.py | 6 ++--- homeassistant/components/ipma/entity.py | 4 ++-- homeassistant/components/ipp/entity.py | 2 +- .../components/islamic_prayer_times/sensor.py | 3 +-- homeassistant/components/iss/sensor.py | 3 +-- homeassistant/components/isy994/__init__.py | 3 +-- .../components/isy994/binary_sensor.py | 2 +- homeassistant/components/isy994/button.py | 2 +- homeassistant/components/isy994/climate.py | 2 +- homeassistant/components/isy994/cover.py | 2 +- homeassistant/components/isy994/entity.py | 3 ++- homeassistant/components/isy994/fan.py | 2 +- homeassistant/components/isy994/helpers.py | 2 +- homeassistant/components/isy994/light.py | 2 +- homeassistant/components/isy994/lock.py | 2 +- homeassistant/components/isy994/models.py | 2 +- homeassistant/components/isy994/number.py | 2 +- homeassistant/components/isy994/select.py | 2 +- homeassistant/components/isy994/sensor.py | 2 +- homeassistant/components/isy994/switch.py | 2 +- homeassistant/components/izone/climate.py | 2 +- homeassistant/components/jellyfin/entity.py | 4 ++-- .../components/jellyfin/media_player.py | 2 +- homeassistant/components/juicenet/entity.py | 2 +- homeassistant/components/justnimbus/entity.py | 2 +- .../components/jvc_projector/entity.py | 2 +- .../components/kaleidescape/entity.py | 3 ++- .../components/keenetic_ndms2/router.py | 2 +- .../components/keymitt_ble/entity.py | 2 +- .../components/kitchen_sink/sensor.py | 2 +- homeassistant/components/kmtronic/switch.py | 2 +- homeassistant/components/knx/device.py | 2 +- homeassistant/components/kodi/media_player.py | 2 +- .../components/konnected/binary_sensor.py | 2 +- homeassistant/components/konnected/sensor.py | 2 +- homeassistant/components/konnected/switch.py | 2 +- .../components/kostal_plenticore/helper.py | 2 +- .../components/kostal_plenticore/number.py | 2 +- .../components/kostal_plenticore/select.py | 2 +- .../components/kostal_plenticore/sensor.py | 2 +- .../components/kostal_plenticore/switch.py | 2 +- homeassistant/components/kraken/sensor.py | 2 +- homeassistant/components/kulersky/light.py | 2 +- .../components/lacrosse_view/sensor.py | 2 +- homeassistant/components/lametric/entity.py | 7 ++++-- .../components/landisgyr_heat_meter/sensor.py | 2 +- homeassistant/components/lastfm/sensor.py | 3 +-- .../components/laundrify/binary_sensor.py | 2 +- homeassistant/components/lcn/__init__.py | 3 ++- .../components/ld2410_ble/binary_sensor.py | 2 +- homeassistant/components/ld2410_ble/sensor.py | 2 +- homeassistant/components/led_ble/light.py | 2 +- homeassistant/components/lidarr/__init__.py | 4 ++-- homeassistant/components/lifx/entity.py | 2 +- homeassistant/components/litejet/light.py | 2 +- homeassistant/components/litejet/scene.py | 2 +- homeassistant/components/litejet/switch.py | 2 +- .../components/litterrobot/entity.py | 3 ++- homeassistant/components/livisi/entity.py | 2 +- .../components/logi_circle/camera.py | 2 +- .../components/logi_circle/sensor.py | 2 +- homeassistant/components/lookin/entity.py | 2 +- homeassistant/components/loqed/entity.py | 3 +-- homeassistant/components/luftdaten/sensor.py | 2 +- .../components/lutron_caseta/__init__.py | 3 ++- .../components/lutron_caseta/binary_sensor.py | 3 +-- .../components/lutron_caseta/button.py | 2 +- .../components/lutron_caseta/models.py | 2 +- .../components/lutron_caseta/scene.py | 2 +- homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/matter/entity.py | 3 ++- homeassistant/components/mazda/__init__.py | 2 +- homeassistant/components/meater/sensor.py | 2 +- homeassistant/components/melcloud/__init__.py | 3 +-- homeassistant/components/melnor/models.py | 2 +- homeassistant/components/met/weather.py | 3 +-- .../components/met_eireann/weather.py | 3 +-- .../components/meteo_france/sensor.py | 3 +-- .../components/meteo_france/weather.py | 3 +-- .../components/meteoclimatic/sensor.py | 3 +-- .../components/meteoclimatic/weather.py | 3 +-- .../components/metoffice/__init__.py | 2 +- homeassistant/components/mill/climate.py | 3 +-- homeassistant/components/mill/sensor.py | 3 +-- .../components/minecraft_server/entity.py | 3 ++- homeassistant/components/mjpeg/camera.py | 2 +- .../components/mobile_app/helpers.py | 2 +- .../components/modern_forms/__init__.py | 2 +- .../components/monoprice/media_player.py | 2 +- homeassistant/components/moon/sensor.py | 3 +-- .../components/motion_blinds/cover.py | 2 +- .../components/motion_blinds/sensor.py | 2 +- .../components/motioneye/__init__.py | 3 ++- homeassistant/components/mqtt/mixins.py | 2 +- .../components/mutesync/binary_sensor.py | 3 +-- homeassistant/components/myq/__init__.py | 2 +- homeassistant/components/mysensors/device.py | 3 ++- homeassistant/components/mystrom/light.py | 2 +- homeassistant/components/mystrom/switch.py | 2 +- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nanoleaf/entity.py | 2 +- homeassistant/components/neato/button.py | 2 +- homeassistant/components/neato/camera.py | 2 +- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/neato/switch.py | 2 +- homeassistant/components/neato/vacuum.py | 2 +- homeassistant/components/nest/camera.py | 2 +- homeassistant/components/nest/climate.py | 2 +- homeassistant/components/nest/device_info.py | 2 +- homeassistant/components/netatmo/climate.py | 2 +- .../components/netatmo/netatmo_entity_base.py | 3 ++- homeassistant/components/netgear/router.py | 3 ++- homeassistant/components/nexia/entity.py | 2 +- homeassistant/components/nextcloud/entity.py | 2 +- homeassistant/components/nextdns/__init__.py | 3 +-- .../components/nibe_heatpump/__init__.py | 3 ++- homeassistant/components/nobo_hub/climate.py | 2 +- homeassistant/components/nobo_hub/sensor.py | 2 +- homeassistant/components/notion/__init__.py | 3 ++- homeassistant/components/nuheat/climate.py | 2 +- homeassistant/components/nuki/__init__.py | 2 +- homeassistant/components/nut/sensor.py | 2 +- homeassistant/components/nws/__init__.py | 3 +-- homeassistant/components/nws/sensor.py | 2 +- homeassistant/components/nws/weather.py | 2 +- .../components/octoprint/__init__.py | 2 +- homeassistant/components/octoprint/button.py | 2 +- homeassistant/components/octoprint/camera.py | 2 +- homeassistant/components/omnilogic/common.py | 2 +- homeassistant/components/oncue/entity.py | 3 ++- homeassistant/components/ondilo_ico/sensor.py | 2 +- homeassistant/components/onewire/model.py | 2 +- .../components/onewire/onewire_entities.py | 3 ++- .../components/onewire/onewirehub.py | 2 +- homeassistant/components/onvif/base.py | 4 ++-- .../components/open_meteo/weather.py | 3 +-- .../components/openexchangerates/sensor.py | 3 +-- homeassistant/components/opengarage/entity.py | 4 ++-- .../components/openhome/media_player.py | 2 +- homeassistant/components/openhome/update.py | 2 +- .../components/opentherm_gw/binary_sensor.py | 3 ++- .../components/opentherm_gw/climate.py | 3 ++- .../components/opentherm_gw/sensor.py | 3 ++- .../components/openweathermap/sensor.py | 3 +-- .../components/openweathermap/weather.py | 3 +-- homeassistant/components/opower/sensor.py | 3 +-- homeassistant/components/overkiz/entity.py | 3 ++- homeassistant/components/overkiz/sensor.py | 2 +- .../components/ovo_energy/__init__.py | 3 +-- .../components/owntracks/device_tracker.py | 2 +- homeassistant/components/p1_monitor/sensor.py | 3 +-- .../panasonic_viera/media_player.py | 2 +- .../components/panasonic_viera/remote.py | 2 +- .../components/pegel_online/entity.py | 2 +- .../components/philips_js/__init__.py | 2 +- homeassistant/components/pi_hole/__init__.py | 2 +- homeassistant/components/picnic/sensor.py | 3 +-- homeassistant/components/plaato/entity.py | 5 ++-- homeassistant/components/plex/button.py | 2 +- homeassistant/components/plex/media_player.py | 3 +-- homeassistant/components/plex/sensor.py | 2 +- homeassistant/components/plugwise/entity.py | 2 +- .../components/plum_lightpad/light.py | 2 +- homeassistant/components/point/__init__.py | 3 ++- .../components/point/alarm_control_panel.py | 2 +- homeassistant/components/powerwall/entity.py | 2 +- .../prosegur/alarm_control_panel.py | 2 +- homeassistant/components/prosegur/camera.py | 2 +- .../components/prusalink/__init__.py | 2 +- homeassistant/components/ps4/media_player.py | 2 +- .../components/pure_energie/sensor.py | 2 +- .../components/purpleair/__init__.py | 2 +- homeassistant/components/pushbullet/sensor.py | 3 +-- homeassistant/components/pvoutput/sensor.py | 2 +- .../components/pvpc_hourly_pricing/sensor.py | 3 +-- homeassistant/components/qnap/sensor.py | 2 +- homeassistant/components/qnap_qsw/entity.py | 4 ++-- homeassistant/components/rachio/entity.py | 3 ++- homeassistant/components/radarr/__init__.py | 4 ++-- homeassistant/components/radiotherm/entity.py | 2 +- .../components/rainbird/coordinator.py | 2 +- homeassistant/components/rainbird/switch.py | 2 +- .../components/rainforest_eagle/sensor.py | 2 +- .../components/rainmachine/__init__.py | 2 +- homeassistant/components/rdw/binary_sensor.py | 3 +-- homeassistant/components/rdw/sensor.py | 3 +-- .../components/recollect_waste/entity.py | 3 +-- .../components/renault/renault_vehicle.py | 2 +- homeassistant/components/renson/entity.py | 2 +- homeassistant/components/reolink/entity.py | 3 +-- homeassistant/components/rfxtrx/__init__.py | 3 ++- homeassistant/components/ridwell/entity.py | 3 +-- homeassistant/components/ring/entity.py | 3 ++- .../components/risco/alarm_control_panel.py | 2 +- homeassistant/components/risco/entity.py | 3 ++- .../rituals_perfume_genie/entity.py | 3 ++- .../components/roborock/coordinator.py | 2 +- homeassistant/components/roborock/device.py | 3 ++- homeassistant/components/roku/entity.py | 4 ++-- .../components/roomba/irobot_base.py | 3 ++- homeassistant/components/roon/media_player.py | 2 +- homeassistant/components/rympro/sensor.py | 2 +- homeassistant/components/sabnzbd/sensor.py | 3 +-- homeassistant/components/samsungtv/entity.py | 3 ++- homeassistant/components/schlage/entity.py | 2 +- homeassistant/components/scrape/sensor.py | 3 +-- .../components/screenlogic/entity.py | 2 +- homeassistant/components/season/sensor.py | 3 +-- homeassistant/components/sense/sensor.py | 2 +- homeassistant/components/sensibo/entity.py | 3 +-- homeassistant/components/senz/climate.py | 2 +- .../components/sfr_box/binary_sensor.py | 2 +- homeassistant/components/sfr_box/button.py | 2 +- homeassistant/components/sfr_box/sensor.py | 2 +- homeassistant/components/sharkiq/vacuum.py | 2 +- homeassistant/components/shelly/button.py | 3 +-- homeassistant/components/shelly/climate.py | 3 +-- homeassistant/components/shelly/entity.py | 4 ++-- .../components/sia/sia_entity_base.py | 3 ++- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/skybell/entity.py | 3 ++- homeassistant/components/slack/__init__.py | 4 ++-- homeassistant/components/sleepiq/entity.py | 3 ++- .../components/slimproto/media_player.py | 2 +- homeassistant/components/sma/__init__.py | 2 +- homeassistant/components/sma/sensor.py | 2 +- .../components/smappee/binary_sensor.py | 2 +- homeassistant/components/smappee/sensor.py | 2 +- homeassistant/components/smappee/switch.py | 2 +- .../components/smartthings/__init__.py | 3 ++- homeassistant/components/smarttub/entity.py | 2 +- homeassistant/components/smhi/weather.py | 3 +-- homeassistant/components/sms/sensor.py | 2 +- homeassistant/components/solarlog/sensor.py | 2 +- homeassistant/components/solax/sensor.py | 2 +- homeassistant/components/soma/__init__.py | 3 ++- .../components/somfy_mylink/cover.py | 2 +- homeassistant/components/sonarr/entity.py | 4 ++-- .../components/songpal/media_player.py | 2 +- homeassistant/components/sonos/entity.py | 3 ++- .../components/soundtouch/media_player.py | 7 ++++-- .../components/speedtestdotnet/sensor.py | 3 +-- homeassistant/components/spider/climate.py | 2 +- homeassistant/components/spider/sensor.py | 2 +- homeassistant/components/spider/switch.py | 2 +- .../components/spotify/media_player.py | 3 +-- homeassistant/components/sql/sensor.py | 3 +-- homeassistant/components/starline/account.py | 2 +- homeassistant/components/starlink/entity.py | 3 ++- .../components/steam_online/entity.py | 3 +-- homeassistant/components/steamist/entity.py | 3 ++- .../components/stookalert/binary_sensor.py | 3 +-- .../components/stookwijzer/sensor.py | 3 +-- homeassistant/components/subaru/__init__.py | 2 +- homeassistant/components/sun/sensor.py | 3 +-- .../components/surepetcare/entity.py | 2 +- .../components/switch_as_x/entity.py | 3 ++- homeassistant/components/switchbee/entity.py | 2 +- homeassistant/components/switchbot/entity.py | 3 ++- .../components/switcher_kis/button.py | 2 +- .../components/switcher_kis/climate.py | 2 +- .../components/switcher_kis/cover.py | 2 +- .../components/switcher_kis/switch.py | 2 +- homeassistant/components/syncthing/sensor.py | 3 +-- .../components/syncthru/binary_sensor.py | 2 +- homeassistant/components/syncthru/sensor.py | 2 +- .../components/synology_dsm/button.py | 2 +- .../components/synology_dsm/camera.py | 2 +- .../components/synology_dsm/entity.py | 3 ++- .../components/synology_dsm/switch.py | 2 +- .../components/system_bridge/__init__.py | 2 +- homeassistant/components/tado/entity.py | 3 ++- .../components/tailscale/__init__.py | 4 ++-- .../components/tankerkoenig/__init__.py | 3 +-- homeassistant/components/tasmota/mixins.py | 4 ++-- homeassistant/components/tautulli/__init__.py | 4 ++-- homeassistant/components/tellduslive/entry.py | 3 ++- .../tesla_wall_connector/__init__.py | 2 +- .../components/threshold/binary_sensor.py | 2 +- homeassistant/components/tibber/sensor.py | 6 +++-- homeassistant/components/tolo/__init__.py | 2 +- .../components/tomorrowio/__init__.py | 3 +-- homeassistant/components/toon/models.py | 2 +- .../totalconnect/alarm_control_panel.py | 2 +- homeassistant/components/tplink/entity.py | 2 +- .../components/tplink_omada/entity.py | 2 +- .../components/traccar/device_tracker.py | 2 +- homeassistant/components/tractive/entity.py | 3 ++- .../components/tradfri/base_class.py | 2 +- .../components/trafikverket_ferry/sensor.py | 3 +-- .../components/trafikverket_train/sensor.py | 3 +-- .../trafikverket_weatherstation/sensor.py | 3 +-- .../components/transmission/sensor.py | 3 +-- .../components/transmission/switch.py | 3 +-- homeassistant/components/tuya/base.py | 3 ++- homeassistant/components/tuya/scene.py | 2 +- .../components/twentemilieu/entity.py | 4 ++-- homeassistant/components/twinkly/light.py | 2 +- .../components/ukraine_alarm/binary_sensor.py | 3 +-- homeassistant/components/unifi/entity.py | 3 ++- homeassistant/components/unifi/switch.py | 3 +-- .../components/unifiprotect/entity.py | 3 ++- homeassistant/components/upb/__init__.py | 3 ++- homeassistant/components/upcloud/__init__.py | 3 +-- homeassistant/components/upnp/entity.py | 3 ++- homeassistant/components/uptime/sensor.py | 3 +-- .../components/uptimerobot/entity.py | 4 ++-- .../components/utility_meter/select.py | 2 +- .../components/utility_meter/sensor.py | 2 +- homeassistant/components/vallox/__init__.py | 2 +- homeassistant/components/velbus/entity.py | 3 ++- homeassistant/components/venstar/__init__.py | 2 +- .../verisure/alarm_control_panel.py | 2 +- .../components/verisure/binary_sensor.py | 3 ++- homeassistant/components/verisure/camera.py | 2 +- homeassistant/components/verisure/lock.py | 2 +- homeassistant/components/verisure/sensor.py | 3 ++- homeassistant/components/verisure/switch.py | 2 +- homeassistant/components/version/entity.py | 4 ++-- homeassistant/components/vesync/common.py | 3 ++- .../components/vicare/binary_sensor.py | 2 +- homeassistant/components/vicare/button.py | 2 +- homeassistant/components/vicare/climate.py | 2 +- homeassistant/components/vicare/sensor.py | 2 +- .../components/vicare/water_heater.py | 2 +- .../components/vizio/media_player.py | 2 +- .../components/vlc_telnet/media_player.py | 3 +-- homeassistant/components/voip/entity.py | 3 ++- .../components/volumio/media_player.py | 2 +- .../components/volvooncall/__init__.py | 2 +- homeassistant/components/vulcan/calendar.py | 4 ++-- homeassistant/components/wallbox/__init__.py | 2 +- .../components/waze_travel_time/sensor.py | 3 +-- .../components/webostv/media_player.py | 2 +- homeassistant/components/wemo/entity.py | 2 +- homeassistant/components/wemo/light.py | 3 +-- homeassistant/components/wemo/wemo_device.py | 2 +- homeassistant/components/whirlpool/climate.py | 3 ++- homeassistant/components/whirlpool/sensor.py | 2 +- homeassistant/components/whois/sensor.py | 3 +-- homeassistant/components/wiffi/__init__.py | 3 ++- homeassistant/components/wilight/__init__.py | 3 ++- homeassistant/components/wiz/entity.py | 4 ++-- homeassistant/components/wled/models.py | 3 +-- .../components/workday/binary_sensor.py | 3 +-- .../components/ws66i/media_player.py | 2 +- homeassistant/components/xbox/base_sensor.py | 3 +-- homeassistant/components/xbox/media_player.py | 2 +- homeassistant/components/xbox/remote.py | 2 +- .../components/xiaomi_aqara/__init__.py | 4 ++-- .../xiaomi_miio/alarm_control_panel.py | 2 +- .../components/xiaomi_miio/device.py | 3 ++- .../components/xiaomi_miio/gateway.py | 3 ++- homeassistant/components/xiaomi_miio/light.py | 2 +- .../components/xiaomi_miio/sensor.py | 2 +- .../components/yale_smart_alarm/entity.py | 4 ++-- homeassistant/components/yalexs_ble/entity.py | 3 ++- .../components/yamaha_musiccast/__init__.py | 7 ++++-- homeassistant/components/yeelight/entity.py | 3 ++- homeassistant/components/yolink/entity.py | 2 +- homeassistant/components/youless/sensor.py | 2 +- homeassistant/components/youtube/entity.py | 4 ++-- homeassistant/components/zamg/sensor.py | 3 +-- homeassistant/components/zamg/weather.py | 3 +-- homeassistant/components/zerproc/light.py | 2 +- homeassistant/components/zeversolar/entity.py | 2 +- homeassistant/components/zha/core/gateway.py | 2 +- .../components/zha/device_tracker.py | 2 +- homeassistant/components/zha/entity.py | 6 ++--- homeassistant/components/zodiac/sensor.py | 3 +-- homeassistant/components/zwave_js/entity.py | 3 ++- homeassistant/components/zwave_js/helpers.py | 2 +- homeassistant/components/zwave_me/__init__.py | 3 ++- homeassistant/helpers/device_registry.py | 21 +++++++++++++++- homeassistant/helpers/entity.py | 24 ++----------------- homeassistant/helpers/sensor.py | 2 +- tests/common.py | 2 +- .../components/assist_pipeline/test_select.py | 2 +- .../test_passive_update_processor.py | 2 +- .../components/kostal_plenticore/conftest.py | 2 +- .../kostal_plenticore/test_helper.py | 2 +- 620 files changed, 821 insertions(+), 800 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 2b7e2a07467..490561c7485 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -28,6 +28,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, entity +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import dispatcher_send from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER @@ -320,9 +321,9 @@ class AbodeDevice(AbodeEntity): } @property - def device_info(self) -> entity.DeviceInfo: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return entity.DeviceInfo( + return DeviceInfo( identifiers={(DOMAIN, self._device.id)}, manufacturer="Abode", model=self._device.type, diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 2a19f0d0291..cdc23fe7e47 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -16,8 +16,7 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index 2fc106f75f5..9ad01ba6f29 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -74,9 +74,9 @@ class AcmedaBase(entity.Entity): return self.roller.id @property - def device_info(self) -> entity.DeviceInfo: + def device_info(self) -> dr.DeviceInfo: """Return the device info.""" - return entity.DeviceInfo( + return dr.DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, manufacturer="Rollease Acmeda", name=self.roller.name, diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 0db6a3615f6..7587bfc0799 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -23,7 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 3a60ad4e8b1..909acd89b80 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -4,8 +4,8 @@ from __future__ import annotations from adguardhome import AdGuardHome, AdGuardHomeError from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 9e4f92e8c98..00750fb4e94 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -4,7 +4,7 @@ from typing import Any from advantage_air import ApiError from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index 7815354dd92..d0aca153d4c 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -4,7 +4,7 @@ from typing import Any from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN diff --git a/homeassistant/components/advantage_air/update.py b/homeassistant/components/advantage_air/update.py index a646ba3b521..8afde183110 100644 --- a/homeassistant/components/advantage_air/update.py +++ b/homeassistant/components/advantage_air/update.py @@ -3,7 +3,7 @@ from homeassistant.components.update import UpdateEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index dc8038862c6..1ac26e2eb79 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONNECTION, DOMAIN as AGENT_DOMAIN diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index d49a1ac387e..cf171987fcb 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -8,7 +8,7 @@ from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index cfbe7b98883..864c36f171a 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -20,8 +20,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 9559d2ecff8..09393741d63 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -17,8 +17,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/airq/coordinator.py b/homeassistant/components/airq/coordinator.py index 78e9580c631..2d0d9d199df 100644 --- a/homeassistant/components/airq/coordinator.py +++ b/homeassistant/components/airq/coordinator.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index 9c9859306ca..cd4e9d52f6b 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 6bcd0337ed1..7f44d71a9fa 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -22,8 +22,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 52e234505c1..882cc1de068 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -18,7 +18,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py index 5bbbb0e895d..3e53fc15b4f 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -23,7 +23,8 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 9ee923ba1af..021aaa3535c 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -26,7 +26,7 @@ from aioairzone.exceptions import AirzoneError from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index 9b3dfdae06c..32c41b8f1cd 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -16,7 +16,7 @@ from aioairzone_cloud.const import ( ) from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 25d601cf299..f466f5f4248 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 395bbbb04a8..e3a1f2d443c 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index cf8b40916f3..2762c3948a7 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -24,7 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 364e5b2abb6..e8c71fcad7a 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -19,11 +19,12 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity import Entity, EntityDescription import homeassistant.helpers.entity_registry as er from .const import ( diff --git a/homeassistant/components/android_ip_webcam/camera.py b/homeassistant/components/android_ip_webcam/camera.py index db6548411a9..92ff29177dd 100644 --- a/homeassistant/components/android_ip_webcam/camera.py +++ b/homeassistant/components/android_ip_webcam/camera.py @@ -11,7 +11,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/android_ip_webcam/entity.py b/homeassistant/components/android_ip_webcam/entity.py index 025132e4bfb..d729da22a9d 100644 --- a/homeassistant/components/android_ip_webcam/entity.py +++ b/homeassistant/components/android_ip_webcam/entity.py @@ -1,7 +1,7 @@ """Base class for Android IP Webcam entities.""" from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 4f927f242df..1fec605d8e1 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -32,9 +32,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle diff --git a/homeassistant/components/androidtv_remote/entity.py b/homeassistant/components/androidtv_remote/entity.py index 5a99805da62..86c8d16260c 100644 --- a/homeassistant/components/androidtv_remote/entity.py +++ b/homeassistant/components/androidtv_remote/entity.py @@ -7,8 +7,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/anova/coordinator.py b/homeassistant/components/anova/coordinator.py index 2e5505a9fdd..436a1e469ba 100644 --- a/homeassistant/components/anova/coordinator.py +++ b/homeassistant/components/anova/coordinator.py @@ -6,7 +6,7 @@ from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate import async_timeout from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 038e71750dd..a28a428a550 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -15,8 +15,8 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ANTHEMAV_UDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index bfe6fe6c80c..164a908e834 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index c1d35c94b4f..818d27bcf77 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -26,11 +26,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py index 5c223940915..b47af54a51f 100644 --- a/homeassistant/components/aranet/sensor.py +++ b/homeassistant/components/aranet/sensor.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 08a65b71193..0173005eb2f 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -19,8 +19,8 @@ from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .config_flow import get_entry_client diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 9cc402e014c..54afc80d451 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -1,7 +1,7 @@ """Aseko entity.""" from aioaseko import Unit -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 8f7229bf5ad..c6fe651d292 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -15,9 +15,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util, slugify diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index e8deca6f04d..5f0552e9d77 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index cdf45db035c..13214b04628 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -16,8 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index 0b7a42267d8..bd81dc0c96f 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -6,7 +6,8 @@ from yalexs.lock import Lock from yalexs.util import get_configuration_url from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from . import DOMAIN, AugustData from .const import MANUFACTURER diff --git a/homeassistant/components/aurora/entity.py b/homeassistant/components/aurora/entity.py index 88ae67daa9e..49afe9fb8c8 100644 --- a/homeassistant/components/aurora/entity.py +++ b/homeassistant/components/aurora/entity.py @@ -2,8 +2,7 @@ import logging -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index 6d3260a45f4..e9ca9e47121 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -7,7 +7,8 @@ from typing import Any from aurorapy.client import AuroraSerialClient -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import ( ATTR_DEVICE_NAME, diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index fa407949b40..aff232f2934 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfInformation, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index ee0febf1455..27962167330 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -27,7 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/axis/entity.py b/homeassistant/components/axis/entity.py index e511ee72d1b..37be5355800 100644 --- a/homeassistant/components/axis/entity.py +++ b/homeassistant/components/axis/entity.py @@ -5,8 +5,9 @@ from abc import abstractmethod from axis.models.event import Event, EventTopic from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 7c63b9ffafa..dc3d0e5b04b 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py index 4aeb287b861..82ea0c16092 100644 --- a/homeassistant/components/baf/entity.py +++ b/homeassistant/components/baf/entity.py @@ -5,8 +5,8 @@ from aiobafi6 import Device from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.entity import Entity class BAFEntity(Entity): diff --git a/homeassistant/components/balboa/entity.py b/homeassistant/components/balboa/entity.py index e50c35db477..2b845b65496 100644 --- a/homeassistant/components/balboa/entity.py +++ b/homeassistant/components/balboa/entity.py @@ -3,8 +3,8 @@ from __future__ import annotations from pybalboa import EVENT_UPDATE, SpaClient -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index a1e646c0b32..371bb1aec40 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -12,7 +12,8 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform 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, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index 75a2644791e..16a8c00d67a 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -14,7 +14,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 1487c6a7b42..1b53a11b1d2 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 9740e427e9c..9f9396c3888 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -9,7 +9,7 @@ from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index c996a90e54d..ceec74a9aa9 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -15,7 +15,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index fa4d76b0cab..20b992d06d6 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -16,7 +16,8 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_platform import async_get_current_platform from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.storage import Store diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 27f2d99cd2d..d5a213256c3 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platf from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery, entity_registry as er import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 36af3974482..3b3ace98950 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -18,7 +18,8 @@ from homeassistant.const import ( ATTR_VIA_DEVICE, ) from homeassistant.core import CALLBACK_TYPE, callback -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later from .const import DOMAIN diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 3cf92a8adcc..5af77f8ee87 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -4,8 +4,8 @@ from __future__ import annotations from boschshcpy import SHCDevice, SHCIntrusionSystem from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get as get_dev_reg -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo, async_get as get_dev_reg +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index a947513e713..0f941d05e75 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -1,5 +1,5 @@ """A entity class for Bravia TV integration.""" -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BraviaTVCoordinator diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index 2c7a05a7e70..ffd0b46e0bf 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -1,7 +1,8 @@ """Broadlink entities.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 191bfff249c..4ea6f7abbad 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index add558ff48b..7d24ebd50b7 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -15,8 +15,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 9f916e5751f..1bde667a237 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -15,7 +15,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index c9b2a2ae9ae..d45749a9a86 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -5,8 +5,8 @@ from bsblan import BSBLAN, Device, Info, StaticState from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 04d8d159541..af78dceca23 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -21,7 +21,7 @@ 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 90cb20a6c6c..bdba9d4f130 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -13,7 +13,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index d32ff07c261..b472b18bed0 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -46,8 +46,8 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.network import NoURLAvailableError, get_url, is_hass_url import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 306ac7f9e3d..aeae8a5afe9 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -14,8 +14,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index ae22fb7b7ef..c5bc7eb4c20 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -13,8 +13,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.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index abd113e71ad..47fd3b91129 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -6,8 +6,7 @@ import logging from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CoinbaseData diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index de4c8208ee0..63cbd9351c7 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/coolmaster/entity.py b/homeassistant/components/coolmaster/entity.py index 1607e220a55..66572a56254 100644 --- a/homeassistant/components/coolmaster/entity.py +++ b/homeassistant/components/coolmaster/entity.py @@ -2,7 +2,7 @@ from pycoolmasternet_async.coolmasternet import CoolMasterNetUnit from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import CoolmasterDataUpdateCoordinator diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 7eb3cfab753..5eb05afd014 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfFrequency from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/crownstone/devices.py b/homeassistant/components/crownstone/devices.py index 83aaac95393..5645d3edd1f 100644 --- a/homeassistant/components/crownstone/devices.py +++ b/homeassistant/components/crownstone/devices.py @@ -3,7 +3,8 @@ from __future__ import annotations from crownstone_cloud.cloud_models.crownstones import Crownstone -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import CROWNSTONE_INCLUDE_TYPES, DOMAIN diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 3ef9c0aba62..dcab26211c9 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -20,8 +20,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.util import Throttle from .const import DOMAIN, KEY_MAC, TIMEOUT diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index ae5f1008820..ef231c45862 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 847f030fae5..4438b83132c 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 7b0c9383cb3..4c0f35266f9 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -11,9 +11,9 @@ from pydeconz.models.scene import Scene as PydeconzScene from pydeconz.models.sensor import SensorBase as PydeconzSensorBase from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DOMAIN as DECONZ_DOMAIN from .gateway import DeconzGateway diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 9f8011e3431..46d10a77271 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -29,7 +29,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import color_hs_to_xy diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 97605d08fe9..63412242dd0 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -17,8 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_WEB_PORT, DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 236d4bbb1b0..21f4054b241 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/button.py b/homeassistant/components/demo/button.py index 3c0498fefef..4fefd75bb8c 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -5,7 +5,7 @@ from homeassistant.components import persistent_notification from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index bfc2cd1a2e7..6639c125653 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -14,7 +14,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 42e30aa8336..93998eb1e8b 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_utc_time_change diff --git a/homeassistant/components/demo/date.py b/homeassistant/components/demo/date.py index 4129d0d392a..34d1909bebe 100644 --- a/homeassistant/components/demo/date.py +++ b/homeassistant/components/demo/date.py @@ -6,7 +6,7 @@ from datetime import date from homeassistant.components.date import DateEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/datetime.py b/homeassistant/components/demo/datetime.py index b769f9baba3..e7f72b66a87 100644 --- a/homeassistant/components/demo/datetime.py +++ b/homeassistant/components/demo/datetime.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone from homeassistant.components.datetime import DateTimeEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/event.py b/homeassistant/components/demo/event.py index e9d26d9f54d..8bc720e2db7 100644 --- a/homeassistant/components/demo/event.py +++ b/homeassistant/components/demo/event.py @@ -4,7 +4,7 @@ from __future__ import annotations from homeassistant.components.event import EventDeviceClass, EventEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index fbc35965dc4..7009df75caa 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 719b1078b8c..5bc0462769d 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -5,7 +5,7 @@ from homeassistant.components.number import NumberDeviceClass, NumberEntity, Num from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/select.py b/homeassistant/components/demo/select.py index 6349b10040c..2a50b0151b6 100644 --- a/homeassistant/components/demo/select.py +++ b/homeassistant/components/demo/select.py @@ -4,7 +4,7 @@ from __future__ import annotations from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index a1f7504762a..41057bc458f 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -22,7 +22,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 49e06839be5..eac267c7c15 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/text.py b/homeassistant/components/demo/text.py index 7c243b73ea5..fecc1b95cf4 100644 --- a/homeassistant/components/demo/text.py +++ b/homeassistant/components/demo/text.py @@ -4,7 +4,7 @@ from __future__ import annotations from homeassistant.components.text import TextEntity, TextMode from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/time.py b/homeassistant/components/demo/time.py index 0384c0822f4..56ab715a7f7 100644 --- a/homeassistant/components/demo/time.py +++ b/homeassistant/components/demo/time.py @@ -6,7 +6,7 @@ from datetime import time from homeassistant.components.time import TimeEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py index 6373c485037..747b3c130d9 100644 --- a/homeassistant/components/demo/update.py +++ b/homeassistant/components/demo/update.py @@ -11,7 +11,7 @@ from homeassistant.components.update import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 67368596439..5674480d493 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -28,7 +28,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CONF_RECEIVER diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index de9f06a0e88..ba77d2a3d4b 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 7d8d0791b4d..50f9acf3e1a 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -17,8 +17,9 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 5848f682626..e63e711ea6f 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -8,7 +8,8 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN from .subscriber import Subscriber diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index e477df63bd2..56a1043d126 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -12,7 +12,8 @@ from devolo_plc_api.device_api import ( from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py index 9d1fd68b742..cd4017eb389 100644 --- a/homeassistant/components/directv/entity.py +++ b/homeassistant/components/directv/entity.py @@ -5,7 +5,8 @@ from typing import cast from directv import DIRECTV -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index 79fc6af1b9a..b243f9adc54 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/dlink/entity.py b/homeassistant/components/dlink/entity.py index bfe16abd780..238db5f5c57 100644 --- a/homeassistant/components/dlink/entity.py +++ b/homeassistant/components/dlink/entity.py @@ -4,7 +4,8 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from .const import ATTRIBUTION, DOMAIN, MANUFACTURER from .data import SmartPlugData diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index bbc15f1b139..ebe5216ab69 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -11,8 +11,7 @@ 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.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 4247015a3b0..ca0958af0ce 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -1,7 +1,8 @@ """The DoorBird integration base entity.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import ( DOORBIRD_INFO_KEY_BUILD_NUMBER, diff --git a/homeassistant/components/dormakaba_dkey/entity.py b/homeassistant/components/dormakaba_dkey/entity.py index 9ec2720d1e8..26a06deed0e 100644 --- a/homeassistant/components/dormakaba_dkey/entity.py +++ b/homeassistant/components/dormakaba_dkey/entity.py @@ -8,7 +8,7 @@ from py_dormakaba_dkey.commands import Notifications from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/dremel_3d_printer/entity.py b/homeassistant/components/dremel_3d_printer/entity.py index 392869a138b..46686e47e1f 100644 --- a/homeassistant/components/dremel_3d_printer/entity.py +++ b/homeassistant/components/dremel_3d_printer/entity.py @@ -2,7 +2,8 @@ from dremel3dpy import Dremel3DPrinter -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 12ad3350e44..3d198e38f36 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -33,7 +33,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import Throttle diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 367eb6cb296..c76a4b72e9a 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -12,7 +12,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/duotecno/entity.py b/homeassistant/components/duotecno/entity.py index f1c72aa55c4..5715593ad2d 100644 --- a/homeassistant/components/duotecno/entity.py +++ b/homeassistant/components/duotecno/entity.py @@ -3,7 +3,8 @@ from __future__ import annotations from duotecno.unit import BaseUnit -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 85c672e0f64..43a4a5b106b 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -7,8 +7,8 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index d673c562bbb..8358887f7a2 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -10,8 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfLength from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/easyenergy/sensor.py b/homeassistant/components/easyenergy/sensor.py index a64851f6696..28bcbbafcb8 100644 --- a/homeassistant/components/easyenergy/sensor.py +++ b/homeassistant/components/easyenergy/sensor.py @@ -21,8 +21,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index e65dc221a9f..f194884f377 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 8c0b77b913d..5e1cff625a4 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -31,7 +31,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_conversion import TemperatureConverter diff --git a/homeassistant/components/ecobee/entity.py b/homeassistant/components/ecobee/entity.py index 4bb2036bb4b..24fe11d17da 100644 --- a/homeassistant/components/ecobee/entity.py +++ b/homeassistant/components/ecobee/entity.py @@ -4,7 +4,8 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from . import EcobeeData from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index fb5533adf07..25a2a56ba93 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -13,7 +13,7 @@ from homeassistant.components.humidifier import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 3996ec6fd35..4d07ec9447e 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 359f9ff485c..729ab463fb3 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -24,7 +24,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index afba9ba6837..3005993bf99 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -16,8 +16,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from .const import API_CLIENT, DOMAIN, EQUIPMENT diff --git a/homeassistant/components/ecowitt/entity.py b/homeassistant/components/ecowitt/entity.py index 76bd89af3d5..12fcca449c0 100644 --- a/homeassistant/components/ecowitt/entity.py +++ b/homeassistant/components/ecowitt/entity.py @@ -5,7 +5,8 @@ import time from aioecowitt import EcoWittSensor -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 1cf611db881..c2fab739789 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -24,11 +24,11 @@ from homeassistant.const import ( UnitOfPower, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo 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.util.dt import utcnow diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index ce8483672a2..0ca5bf1d8f7 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -9,7 +9,8 @@ from homeassistant.core import HomeAssistant 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.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 2642505fbea..b8066f2eb31 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -24,8 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import async_get -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo, async_get from homeassistant.helpers.typing import UNDEFINED, ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/electrasmart/climate.py b/homeassistant/components/electrasmart/climate.py index 59523d5a4cb..086a5288f77 100644 --- a/homeassistant/components/electrasmart/climate.py +++ b/homeassistant/components/electrasmart/climate.py @@ -28,7 +28,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/elgato/entity.py b/homeassistant/components/elgato/entity.py index 041a3196df5..4f4c2a9d8e9 100644 --- a/homeassistant/components/elgato/entity.py +++ b/homeassistant/components/elgato/entity.py @@ -2,8 +2,11 @@ from __future__ import annotations from homeassistant.const import CONF_MAC -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index c20621ce60f..49e35a127fe 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -31,8 +31,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 797121b6e46..fc08895ba4d 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -22,7 +22,7 @@ from elmax_api.model.panel import PanelEntry, PanelStatus from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 0cf4f0f2346..6e196eebeb0 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import UNDEFINED, StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py index 17052dfab57..2d3a8954220 100644 --- a/homeassistant/components/energyzero/sensor.py +++ b/homeassistant/components/energyzero/sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CURRENCY_EURO, PERCENTAGE, UnitOfEnergy, UnitOfVolume from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index a5ebb476c59..68368719fc4 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 2b2dd591faa..9ecc205522a 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -30,7 +30,8 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index a8548429d50..64a4b7dad20 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -9,8 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index a0d1476aea7..5c49f566bb5 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -37,7 +37,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index 0c85705a2a6..71c8a403f8f 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -18,8 +18,8 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index fceb2778734..57ae33beb15 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -19,8 +19,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from .domain_data import DomainData diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 2ac69c3a22d..859b28a53b5 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -16,8 +16,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/eufylife_ble/sensor.py b/homeassistant/components/eufylife_ble/sensor.py index 741f71b34d2..7bc732b911e 100644 --- a/homeassistant/components/eufylife_ble/sensor.py +++ b/homeassistant/components/eufylife_ble/sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util.unit_conversion import MassConverter diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index 839d546588c..81a29b1432e 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py index 906739da4b3..3ce33028629 100644 --- a/homeassistant/components/ezviz/alarm_control_panel.py +++ b/homeassistant/components/ezviz/alarm_control_panel.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index d3720170c29..6fad2b57142 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -3,8 +3,8 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index dc7be9f1e69..86f25253c2d 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -26,7 +26,8 @@ from homeassistant.exceptions import ( HomeAssistantError, ) from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from .const import CONF_IMPORT_PLUGINS, DOMAIN diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index 43baa0e4efd..812a85b2f50 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -9,7 +9,7 @@ from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 49f14e0031a..0526df81a02 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_FILE_PATH, EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 33e23f8d401..51d2ad51866 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .board import FirmataPinType from .const import DOMAIN, FIRMATA_MANUFACTURER diff --git a/homeassistant/components/fivem/entity.py b/homeassistant/components/fivem/entity.py index cfd9d502b2f..c11378ff049 100644 --- a/homeassistant/components/fivem/entity.py +++ b/homeassistant/components/fivem/entity.py @@ -6,7 +6,8 @@ from dataclasses import dataclass import logging from typing import Any -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index e867e624e8a..48d7809b715 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -19,11 +19,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_DETECTION, DOMAIN diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index 8b641013eb4..41cdc0dbffe 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -13,7 +13,8 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/fjaraskupan/coordinator.py b/homeassistant/components/fjaraskupan/coordinator.py index 16e8157b094..f955c7ca024 100644 --- a/homeassistant/components/fjaraskupan/coordinator.py +++ b/homeassistant/components/fjaraskupan/coordinator.py @@ -15,7 +15,7 @@ from homeassistant.components.bluetooth import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index e19a0965524..142694a6bfb 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -15,7 +15,7 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.percentage import ( diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index f4aa8c5a2dc..396f6b00e3b 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -8,7 +8,8 @@ from fjaraskupan import COMMAND_LIGHT_ON_OFF from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index 46c5f6db90b..d57e10aa561 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -5,7 +5,8 @@ from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index e9bf84e0ed0..30527d4e29d 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -11,7 +11,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +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 diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 9eba3206720..81c21a4aa99 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -8,7 +8,8 @@ from flipr_api.exceptions import FliprError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index e9d02432598..ef8a04440d1 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -3,8 +3,8 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN as FLO_DOMAIN from .device import FloDeviceDataUpdateCoordinator diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index ef63eeff1d7..f17e58529c4 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import TypeVar -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 85600dd4dab..1adcd39e22f 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -20,8 +20,9 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_MINOR_VERSION, DOMAIN, SIGNAL_STATE_UPDATED diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 1b511f03eda..7a2723ce591 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -18,8 +18,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py index 70db52cc127..bc40d9560d6 100644 --- a/homeassistant/components/freebox/button.py +++ b/homeassistant/components/freebox/button.py @@ -12,7 +12,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/freebox/home_base.py b/homeassistant/components/freebox/home_base.py index c74f072a5be..37709cbf494 100644 --- a/homeassistant/components/freebox/home_base.py +++ b/homeassistant/components/freebox/home_base.py @@ -5,8 +5,9 @@ import logging from typing import Any from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import CATEGORY_TO_MODEL, DOMAIN from .router import FreeboxRouter diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 122242f1959..6111eb85b4c 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -18,9 +18,8 @@ from freebox_api.exceptions import HttpRequestError, NotOpenError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.storage import Store from homeassistant.util import slugify diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 488d2d48f8c..7290ce47c04 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -12,8 +12,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index 9f397d32899..3bba3439341 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index 8a0a706c0d9..7a4b0473600 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -18,7 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index 59e58d75c43..b57acfacb4f 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 68149b65fd7..59eb50ebe4a 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 2a101d5c82a..9df3679ad70 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index e1e8ee44b2d..b1544d9b20d 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 85d70c30956..dc6861a4f0a 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index 7313be1920c..4de27c270b4 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py index d76279a0f14..a4504996820 100644 --- a/homeassistant/components/fritz/button.py +++ b/homeassistant/components/fritz/button.py @@ -14,8 +14,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import AvmWrapper diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 531c05eea4a..d61ce334804 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -36,8 +36,9 @@ from homeassistant.helpers import ( entity_registry as er, update_coordinator, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 1352d9cb42e..026c0f3d6fb 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -9,9 +9,9 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index bd246dd914f..54e09f90df7 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -17,7 +17,8 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py index b9f94e24dc0..cc5457fb8a2 100644 --- a/homeassistant/components/fritzbox/button.py +++ b/homeassistant/components/fritzbox/button.py @@ -4,7 +4,7 @@ from pyfritzhome.devicetypes import FritzhomeTemplate from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzboxDataUpdateCoordinator, FritzBoxEntity diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index adf6bd3a35a..43cdb29f85f 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import FritzBoxPhonebook diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 6202b945d97..793f381d52f 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -15,8 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .const import ( diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py index b65864ee089..4060731b21c 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -1,7 +1,7 @@ """Constants for the Fronius integration.""" from typing import Final, NamedTuple, TypedDict -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo DOMAIN: Final = "fronius" diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ff949af0cba..6d5e43a94ee 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -24,8 +24,8 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo 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 StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 490cc89febc..641a267e987 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -21,7 +21,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .browse_media import browse_node, browse_top_level diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py index d1f98c5afff..2fe367643ee 100644 --- a/homeassistant/components/fully_kiosk/entity.py +++ b/homeassistant/components/fully_kiosk/entity.py @@ -1,8 +1,8 @@ """Base entity for the Fully Kiosk Browser integration.""" from __future__ import annotations -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index 2390f5af561..df41b0a1c43 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo import homeassistant.util.dt as dt_util from .const import DOMAIN diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index 9f5dc3223b5..67ed056f7b1 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -15,7 +15,8 @@ from gardena_bluetooth.parse import Characteristic, CharacteristicType from homeassistant.components import bluetooth from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index d1be775e370..541d2e0b89d 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -10,8 +10,7 @@ from geocachingapi.models import GeocachingStatus from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 66cbbcbd67e..f0159915d32 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -4,8 +4,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 64119436230..f5bbdb06198 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -18,8 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index edcdd8c057b..d497700f5db 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -13,8 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index e952164792f..cd9c3a9135d 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/goalzero/entity.py b/homeassistant/components/goalzero/entity.py index eef6ea43d9c..d72d1557a03 100644 --- a/homeassistant/components/goalzero/entity.py +++ b/homeassistant/components/goalzero/entity.py @@ -4,7 +4,8 @@ from goalzero import Yeti from homeassistant.const import ATTR_MODEL, CONF_NAME from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN, MANUFACTURER diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index d45a4fb44ec..4a811373cb1 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -24,7 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/goodwe/__init__.py b/homeassistant/components/goodwe/__init__.py index b5872ed3dea..02c1d5beac7 100644 --- a/homeassistant/components/goodwe/__init__.py +++ b/homeassistant/components/goodwe/__init__.py @@ -6,7 +6,7 @@ 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.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import ( CONF_MODEL_FAMILY, diff --git a/homeassistant/components/goodwe/button.py b/homeassistant/components/goodwe/button.py index 4ac61c59e58..55ba33b63f6 100644 --- a/homeassistant/components/goodwe/button.py +++ b/homeassistant/components/goodwe/button.py @@ -10,7 +10,7 @@ from homeassistant.components.button import ButtonEntity, ButtonEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 3f9714aa372..7e31dd14037 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -15,7 +15,7 @@ from homeassistant.components.number import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index bfaef5d537a..012d73f792c 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -7,7 +7,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index 4a4296bc526..332280bac5a 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/google_assistant/button.py b/homeassistant/components/google_assistant/button.py index 47681308b53..94c97357b85 100644 --- a/homeassistant/components/google_assistant/button.py +++ b/homeassistant/components/google_assistant/button.py @@ -6,7 +6,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py index 5e447125e82..fed8ff481f0 100644 --- a/homeassistant/components/google_mail/entity.py +++ b/homeassistant/components/google_mail/entity.py @@ -1,8 +1,8 @@ """Entity representing a Google Mail account.""" from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from .api import AsyncConfigEntryAuth from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index ffbc4ff3cfd..65db01cde59 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -16,8 +16,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.location import find_coordinates import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 4cce9290a68..278d6571cb7 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -9,8 +9,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 8a53e3b3229..87c3fcf7eed 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -37,9 +37,8 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index 66be66f9dc9..ea3aa28ac13 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -1,6 +1,5 @@ """Entity object for shared properties of Gree entities.""" -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .bridge import DeviceDataUpdateCoordinator diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 87822227cef..06d06ed26ce 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle, dt as dt_util diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index ec8bd818d38..d7a9fe4e836 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -23,8 +23,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index a73f0822d77..a1b11189a04 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -11,7 +11,7 @@ from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import ACTIVITY_POWER_OFF from .subscriber import HarmonySubscriberMixin diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 08349c0f467..0abc484b798 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -41,7 +41,7 @@ from homeassistant.helpers import ( recorder, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 3a6a5a9f7c3..6530aba3ea1 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index c111a23bf06..e2487e90a99 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -23,11 +23,11 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo 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.util.dt import utcnow diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 537f782ad09..193a86a3d37 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -22,8 +22,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.start import async_at_started from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 76d75e51725..ba060caa43a 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -17,12 +17,12 @@ from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 3c2ac52929a..12fe7be3be9 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -3,8 +3,9 @@ import logging from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .api import HomeConnectDevice from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py index 99766ebfec9..ef2c1447bf4 100644 --- a/homeassistant/components/home_plus_control/switch.py +++ b/homeassistant/components/home_plus_control/switch.py @@ -6,7 +6,7 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import dispatcher -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 4ba22317644..3e5fd4655d6 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -26,7 +26,7 @@ from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, c from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .config_flow import normalize_hkid diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 6ebe777d5f8..6fdb450a5b4 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -12,7 +12,8 @@ from aiohomekit.model.characteristics import ( ) from aiohomekit.model.services import Service, ServicesTypes -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .connection import HKDevice, valid_serial_number diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 1b29fcb1068..1a2f2293c1c 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as HMIPC_DOMAIN diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index fb4bfdd637e..6730f722685 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -35,7 +35,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 7aa68709040..6e4959a4789 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -26,7 +26,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 199cbacfa15..46d036c777b 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -10,7 +10,8 @@ from homematicip.aio.group import AsyncGroup from homeassistant.const import ATTR_ID from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN as HMIPC_DOMAIN from .hap import AsyncHome, HomematicipHAP diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py index 2aa1b0369d9..3279c9ba41b 100644 --- a/homeassistant/components/homewizard/entity.py +++ b/homeassistant/components/homewizard/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.const import ATTR_IDENTIFIERS -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index db31baa53a6..19eb5c649d7 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -26,7 +26,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HoneywellData diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index ae4ede2a079..8c25216b2ff 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 3c101dff9cc..f21f084a544 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -46,8 +46,9 @@ from homeassistant.helpers import ( discovery, entity_registry as er, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 2c6c1679779..bd290d0bbb8 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -13,8 +13,7 @@ import voluptuous as vol from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index f0ba0dbac23..8821c66a2cf 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -28,7 +28,7 @@ from homeassistant.components.light import ( from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/hue/v1/sensor_device.py b/homeassistant/components/hue/v1/sensor_device.py index 176b5f118b2..9ffc1518cba 100644 --- a/homeassistant/components/hue/v1/sensor_device.py +++ b/homeassistant/components/hue/v1/sensor_device.py @@ -1,5 +1,6 @@ """Support for the Philips Hue sensor devices.""" from homeassistant.helpers import entity +from homeassistant.helpers.device_registry import DeviceInfo from ..const import ( CONF_ALLOW_UNREACHABLE, @@ -47,12 +48,12 @@ class GenericHueDevice(entity.Entity): return self.primary_sensor.raw.get("swupdate", {}).get("state") @property - def device_info(self) -> entity.DeviceInfo: + def device_info(self) -> DeviceInfo: """Return the device info. Links individual entities together in the hass device registry. """ - return entity.DeviceInfo( + return DeviceInfo( identifiers={(HUE_DOMAIN, self.device_id)}, manufacturer=self.primary_sensor.manufacturername, model=(self.primary_sensor.productname or self.primary_sensor.modelid), diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index ef01b2e4693..f4c76618009 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -9,8 +9,11 @@ from aiohue.v2.models.resource import ResourceTypes from aiohue.v2.models.zigbee_connectivity import ConnectivityServiceStatus from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get as async_get_device_registry -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import ( + DeviceInfo, + async_get as async_get_device_registry, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from ..bridge import HueBridge diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 1cb862e3d77..9985d37627b 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -21,8 +21,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 655eb572c31..08f3c749fc5 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -4,7 +4,7 @@ from aiopvapi.resources.shade import ATTR_TYPE, BaseShade from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index a50b2c4d09b..ac965285977 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index c58ae6e3931..76a7966a6ed 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -11,8 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone, utcnow diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 0366816ef1a..f36b84170a9 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -27,11 +27,11 @@ from homeassistant.components.camera import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo 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 . import ( diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index e14c395315e..54f9a3a27ff 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -19,11 +19,11 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index dde2a5c29c5..95f14b9b888 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -28,11 +28,11 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo 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.util import slugify diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 1981a56e211..b09e31f5312 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -7,7 +7,7 @@ from homeassistant.components.alarm_control_panel import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 5735e1ab421..9554d30df45 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -29,11 +29,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN, UPDATE_INTERVAL diff --git a/homeassistant/components/ibeacon/entity.py b/homeassistant/components/ibeacon/entity.py index 4baa06dd617..b25c82037e1 100644 --- a/homeassistant/components/ibeacon/entity.py +++ b/homeassistant/components/ibeacon/entity.py @@ -6,8 +6,9 @@ from abc import abstractmethod from ibeacon_ble import iBeaconAdvertisement from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import ATTR_MAJOR, ATTR_MINOR, ATTR_SOURCE, ATTR_UUID, DOMAIN from .coordinator import IBeaconCoordinator, signal_seen, signal_unavailable diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 6cabe51fff5..0bd1dfb44a9 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -6,8 +6,8 @@ from typing import Any from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .account import IcloudAccount, IcloudDevice diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 01aabc5871c..e92a9ae4a8d 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -7,8 +7,8 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index cd6da667ccb..92a66fabe49 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -9,8 +9,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index dd7acc65458..d1762fa8d35 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -5,11 +5,12 @@ import logging from pyinsteon import devices from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import ( DOMAIN, diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 5ce64de9b33..6daecc6a305 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -32,7 +32,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 5003ed91437..f9502f70ee7 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -9,7 +9,7 @@ from intellifire4py import IntellifirePollData from intellifire4py.intellifire import IntellifireAPILocal from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index f3767be9f3d..45cd3586af2 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index b616c7e4ae9..27ecc1574e3 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -24,7 +24,7 @@ from homeassistant.const import ( UnitOfPower, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity, entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -182,9 +182,9 @@ class IotaWattSensor(CoordinatorEntity[IotawattUpdater], SensorEntity): return self._sensor_data.getName() @property - def device_info(self) -> entity.DeviceInfo: + def device_info(self) -> dr.DeviceInfo: """Return device info.""" - return entity.DeviceInfo( + return dr.DeviceInfo( connections={ (dr.CONNECTION_NETWORK_MAC, self._sensor_data.hub_mac_address) }, diff --git a/homeassistant/components/ipma/entity.py b/homeassistant/components/ipma/entity.py index bc8136b6206..6424084c533 100644 --- a/homeassistant/components/ipma/entity.py +++ b/homeassistant/components/ipma/entity.py @@ -1,8 +1,8 @@ """Base Entity for IPMA.""" from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index 50f81f74bdb..2ce6b0f3fa0 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -1,7 +1,7 @@ """Entities for The Internet Printing Protocol (IPP) integration.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 2552be7717a..ee3c5d9071d 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -8,8 +8,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/iss/sensor.py b/homeassistant/components/iss/sensor.py index 32516ee99c9..d7b7083cdef 100644 --- a/homeassistant/components/iss/sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -8,8 +8,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index e6e23fdf837..f19e21b4f6d 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -23,8 +23,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from .const import ( _LOGGER, diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 621b17f096e..32fa72e5565 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -21,7 +21,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 3eba58aa0aa..1ccc3acf659 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -17,7 +17,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_NETWORK, DOMAIN diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 8fc90efaabc..8b244004da3 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -35,7 +35,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 4504cde713e..1b1b5e226f7 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 425f1fe5b87..80319b83ba2 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -23,7 +23,8 @@ from pyisy.variables import Variable from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 75c033bd9ea..99e359b27b9 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -10,7 +10,7 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 611d0467710..5e0ff592ea9 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -24,7 +24,7 @@ from pyisy.nodes import Group, Node, Nodes from pyisy.programs import Programs from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, Platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import ( _LOGGER, diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 8c64e5b9d55..a1fa8975e79 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -11,7 +11,7 @@ from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 9bf487def07..81c7e925af2 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, diff --git a/homeassistant/components/isy994/models.py b/homeassistant/components/isy994/models.py index 202bebb32f8..c8a7e1dbefe 100644 --- a/homeassistant/components/isy994/models.py +++ b/homeassistant/components/isy994/models.py @@ -12,7 +12,7 @@ from pyisy.programs import Program from pyisy.variables import Variable from homeassistant.const import Platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import ( CONF_NETWORK, diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index e8defd4592c..7448ba7ff27 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( percentage_to_ranged_value, diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py index 60e2111848d..3c55e5cbda9 100644 --- a/homeassistant/components/isy994/select.py +++ b/homeassistant/components/isy994/select.py @@ -32,7 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 5f36fed6b6a..a994e1dadef 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -31,7 +31,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 62ae375736d..39b84faad30 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -23,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 4c9eb3a607c..cac29641e28 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -30,8 +30,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/jellyfin/entity.py b/homeassistant/components/jellyfin/entity.py index eb74b5d5c51..e45557fa4b6 100644 --- a/homeassistant/components/jellyfin/entity.py +++ b/homeassistant/components/jellyfin/entity.py @@ -1,8 +1,8 @@ """Base Entity for Jellyfin.""" from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index bcd8e975823..76343818702 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import parse_datetime diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 2f25a934e7f..3c325715c82 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -2,7 +2,7 @@ from pyjuicenet import Charger -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py index 67575809135..968e9581a67 100644 --- a/homeassistant/components/justnimbus/entity.py +++ b/homeassistant/components/justnimbus/entity.py @@ -1,7 +1,7 @@ """Base Entity for JustNimbus sensors.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/jvc_projector/entity.py b/homeassistant/components/jvc_projector/entity.py index 5d1821c6b56..a88fba03cb0 100644 --- a/homeassistant/components/jvc_projector/entity.py +++ b/homeassistant/components/jvc_projector/entity.py @@ -6,7 +6,7 @@ import logging from jvcprojector import JvcProjector -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, NAME diff --git a/homeassistant/components/kaleidescape/entity.py b/homeassistant/components/kaleidescape/entity.py index 87a9fa4da0e..667cba757d6 100644 --- a/homeassistant/components/kaleidescape/entity.py +++ b/homeassistant/components/kaleidescape/entity.py @@ -6,7 +6,8 @@ import logging from typing import TYPE_CHECKING from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN as KALEIDESCAPE_DOMAIN, NAME as KALEIDESCAPE_NAME diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index fdab10ea55e..77101dcbf3e 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -18,8 +18,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_call_later import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py index 31315e59efb..b61f8a3c24d 100644 --- a/homeassistant/components/keymitt_ble/entity.py +++ b/homeassistant/components/keymitt_ble/entity.py @@ -7,7 +7,7 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import MANUFACTURER from .coordinator import MicroBotDataUpdateCoordinator diff --git a/homeassistant/components/kitchen_sink/sensor.py b/homeassistant/components/kitchen_sink/sensor.py index 6912c940482..4e1e3bd2010 100644 --- a/homeassistant/components/kitchen_sink/sensor.py +++ b/homeassistant/components/kitchen_sink/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index ed54315de90..cd1b181803f 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -5,7 +5,7 @@ import urllib.parse from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/knx/device.py b/homeassistant/components/knx/device.py index 18e6197360a..583ca2f768b 100644 --- a/homeassistant/components/knx/device.py +++ b/homeassistant/components/knx/device.py @@ -8,7 +8,7 @@ from xknx.io.gateway_scanner import GatewayDescriptor from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 4a7f30506b2..9c69abc08c8 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -44,7 +44,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_platform, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import is_internal_request diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index a4ceed5c50d..2f21f8c15bd 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -10,8 +10,8 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as KONNECTED_DOMAIN diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 749e1d5fd82..b341afa765f 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -17,8 +17,8 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_DS18B20_NEW diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index c5a0ca712e5..ba0dc62b606 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -13,8 +13,8 @@ from homeassistant.const import ( CONF_ZONE, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 35ec7bb9456..1c495ac9db9 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_ST from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 885b19faf28..834057d63b8 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -16,7 +16,7 @@ from homeassistant.components.number import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 1a89e5617cc..779cc24b0c4 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -9,7 +9,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 036f2baf98e..78ab609aa16 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -22,7 +22,7 @@ from homeassistant.const import ( UnitOfPower, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 4427f4bd4e1..574368b432f 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -10,7 +10,7 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index 4bbf232f84b..87ad8dc258f 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -13,8 +13,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 91f19dbdd08..c68633ab639 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 547772cad09..76688af61ae 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -23,7 +23,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/lametric/entity.py b/homeassistant/components/lametric/entity.py index 35810df0273..54626a3838d 100644 --- a/homeassistant/components/lametric/entity.py +++ b/homeassistant/components/lametric/entity.py @@ -1,8 +1,11 @@ """Base entity for the LaMetric integration.""" from __future__ import annotations -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 9669648b4c5..8ef81e899b7 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 0b2039436f4..f0f3af3b672 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -11,8 +11,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/laundrify/binary_sensor.py b/homeassistant/components/laundrify/binary_sensor.py index 3e865bd4c0c..81882b68f00 100644 --- a/homeassistant/components/laundrify/binary_sensor.py +++ b/homeassistant/components/laundrify/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 7ef7eb73673..527c3de7c9e 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -21,7 +21,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .const import ( diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py index 59580d5725e..cca87de7a60 100644 --- a/homeassistant/components/ld2410_ble/binary_sensor.py +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py index 806832e9fca..5bd4a0d4d2d 100644 --- a/homeassistant/components/ld2410_ble/sensor.py +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfLength from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py index 94f445f1ec1..5fba73ef808 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py index 9222164227b..dd63920b209 100644 --- a/homeassistant/components/lidarr/__init__.py +++ b/homeassistant/components/lidarr/__init__.py @@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/lifx/entity.py b/homeassistant/components/lifx/entity.py index 5f08b6e7884..4bc6b87393d 100644 --- a/homeassistant/components/lifx/entity.py +++ b/homeassistant/components/lifx/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from aiolifx import products from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 9b771bdc035..167f7a62a00 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_DEFAULT_TRANSITION, DOMAIN diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 83eb2cc5f0b..ce04a537559 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -8,7 +8,7 @@ from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 97a51223429..025770cdc35 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -7,7 +7,7 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 063799868b6..fb1fbe58a7b 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -6,7 +6,8 @@ from typing import Generic, TypeVar from pylitterbot import Robot from pylitterbot.robot import EVENT_UPDATE -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/livisi/entity.py b/homeassistant/components/livisi/entity.py index ebd2b813852..5ddba1e2e86 100644 --- a/homeassistant/components/livisi/entity.py +++ b/homeassistant/components/livisi/entity.py @@ -8,8 +8,8 @@ from aiolivisi.const import CAPABILITY_MAP from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LIVISI_REACHABILITY_CHANGE diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 4a8b36a3d55..027009669e5 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -15,8 +15,8 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo 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, DiscoveryInfoType diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index b27ba30128f..148dd88b41a 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index 35de968cf2f..d20a21bd23c 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -14,7 +14,7 @@ from aiolookin import ( ) from aiolookin.models import Device, UDPCommandType, UDPEvent -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MODEL_NAMES diff --git a/homeassistant/components/loqed/entity.py b/homeassistant/components/loqed/entity.py index 978fe844d61..aec50ec8f92 100644 --- a/homeassistant/components/loqed/entity.py +++ b/homeassistant/components/loqed/entity.py @@ -1,8 +1,7 @@ """Base entity for the LOQED integration.""" from __future__ import annotations -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index cca467ce756..58fa5788bda 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -20,7 +20,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index da2c03745fa..0a6a2aa8211 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -19,7 +19,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .const import ( diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 29e59c426b5..334590c0e65 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -8,8 +8,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_name_from_id diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py index 1c01ed651fd..d31e4579675 100644 --- a/homeassistant/components/lutron_caseta/button.py +++ b/homeassistant/components/lutron_caseta/button.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDevice diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 61f00a1b09f..91b042106cb 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -7,7 +7,7 @@ from typing import Any, Final, TypedDict from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo @dataclass diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 997397c5b6c..520dcd965f2 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -6,7 +6,7 @@ from pylutron_caseta.smartbridge import Smartbridge from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as CASETA_DOMAIN diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index c2423a7c47f..c2c1c9ae77a 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 0082370d5ff..102e0c83b7b 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -12,7 +12,8 @@ from matter_server.common.helpers.util import create_attribute_path from matter_server.common.models import EventType, ServerInfoMessage from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN, ID_TYPE_DEVICE_ID from .helpers import get_device_id diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index bb92496b74f..1322a7db300 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index cf71455a81b..98bb44947c8 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index eea169c3591..2d7354f250f 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -17,8 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py index 8cbe5f80680..409cb9ae3ba 100644 --- a/homeassistant/components/melnor/models.py +++ b/homeassistant/components/melnor/models.py @@ -9,7 +9,7 @@ from melnor_bluetooth.device import Device, Valve from homeassistant.components.number import EntityDescription from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index d364066ae61..500cb3c5716 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -29,8 +29,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.unit_system import METRIC_SYSTEM diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 67c0a830c61..1356dbe0c24 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -20,8 +20,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 67fcd9d71fc..98cb4665614 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -28,8 +28,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 165cefc9240..a2e9dc30c53 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -24,8 +24,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 73804c1f77a..ed37c6d98ea 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -14,8 +14,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 11346ab18f9..d275707488b 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -5,8 +5,7 @@ from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 695c6c8f47d..56bf5ee99ce 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 975bb2ff2c7..2ddcf97f25a 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -20,8 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 843e8b6570e..47b5b8c7b64 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -22,8 +22,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/minecraft_server/entity.py b/homeassistant/components/minecraft_server/entity.py index 02875cb69f2..9458a3ef397 100644 --- a/homeassistant/components/minecraft_server/entity.py +++ b/homeassistant/components/minecraft_server/entity.py @@ -1,8 +1,9 @@ """Base entity for the Minecraft Server integration.""" from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from . import MinecraftServer from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index c2ab3b5768c..dab5b477ede 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -26,7 +26,7 @@ from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_web, async_get_clientsession, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index f14223f4a04..741b0a400cc 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -13,7 +13,7 @@ from nacl.secret import SecretBox from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.json import JSONEncoder from homeassistant.util.json import JsonValueType, json_loads diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index d7f30ce5c3b..fafd7f9c8d2 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 5a61e306991..92b98abf374 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform, service -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 56b5fa52325..b7c5b8d7726 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -6,8 +6,7 @@ from astral import moon from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 17918133614..c9578380048 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -21,7 +21,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_platform, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 360463d678f..bca1c1ef1dd 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 2876a4d49a1..59fc41df9b0 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -51,11 +51,12 @@ 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.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 70156703155..fc87971064e 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -36,6 +36,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import ( DeviceEntry, + DeviceInfo, EventDeviceRegistryUpdatedData, ) from homeassistant.helpers.dispatcher import ( @@ -44,7 +45,6 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import ( ENTITY_CATEGORIES_SCHEMA, - DeviceInfo, Entity, async_generate_entity_id, ) diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py index 3c9d92094f7..444643d5333 100644 --- a/homeassistant/components/mutesync/binary_sensor.py +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -3,8 +3,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index d5b4730c2de..c50ea579a14 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 42c5a40636e..a89de3abf69 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -11,8 +11,9 @@ from mysensors.sensor import ChildSensor from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import ( CHILD_CALLBACK, diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index d32a64dc1e6..6a6e7efa1b3 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -20,7 +20,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 54c1dc9ad5a..262ee54101b 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 73276017254..5004bafeb1b 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -23,7 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index 16fb746049d..73d635a46a1 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -2,7 +2,7 @@ from aionanoleaf import Nanoleaf -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/neato/button.py b/homeassistant/components/neato/button.py index ba0438998f6..4bbd9196932 100644 --- a/homeassistant/components/neato/button.py +++ b/homeassistant/components/neato/button.py @@ -7,7 +7,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_ROBOTS diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index da50e528d3c..5b13d12d37f 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -12,7 +12,7 @@ from urllib3.response import HTTPResponse from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 2b8e0b3bf8b..60aeb52af05 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 6fba5327290..f6d159fcc1b 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index b10b1f83eac..f70e79f3fc0 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -22,7 +22,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE, STATE_IDLE, STATE_PAUSED from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 3f8c99d7658..721af504fd8 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry 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 diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 307bd201b4d..02874cab84c 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -32,7 +32,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_DEVICE_MANAGER, DOMAIN diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 891365655de..1bdb60ee1b4 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -9,7 +9,7 @@ from google_nest_sdm.device_traits import ConnectivityTrait, InfoTrait from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import CONNECTIVITY_TRAIT_OFFLINE, DATA_DEVICE_MANAGER, DOMAIN diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 94844578d9d..9f34df9b39c 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -27,8 +27,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index ff6783ecaa3..4cf5766b6b5 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -10,7 +10,8 @@ from pyatmo.modules.device_types import ( from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME from .data_handler import PUBLIC, NetatmoDataHandler diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 98958fbbb9b..2dc86833003 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -19,7 +19,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 6b017db4d34..2a09ec877c8 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -9,11 +9,11 @@ from homeassistant.const import ( ATTR_SUGGESTED_AREA, ATTR_VIA_DEVICE, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 4308e573859..17d59fe6b29 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -1,6 +1,6 @@ """Base entity for the Nextcloud integration.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 72b3f52d3e8..3865136b2ac 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -27,8 +27,7 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index a38e2182ad7..01a51f015d9 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -25,7 +25,8 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index b22206734f8..e3cfa04802c 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 3313aaf4ce7..3446f1ea43b 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 258f14056ca..88605fdbdfd 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -31,7 +31,8 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index b2bc66b60c0..18b34ea0bea 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -21,7 +21,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import event as event_helper -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index d237303e7c9..f72abc410ef 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -30,7 +30,7 @@ from homeassistant.helpers import ( entity_registry as er, issue_registry as ir, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 6574577558e..9151a86a9f8 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -28,7 +28,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index ef0731ee94c..54c239664dc 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -13,8 +13,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Pla from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 79a4294449b..71eeda0d8cf 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 8ddf842cd62..0c491723117 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -29,7 +29,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index dd6ab5794fc..1ca0dc1f5d5 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify as util_slugify diff --git a/homeassistant/components/octoprint/button.py b/homeassistant/components/octoprint/button.py index 0d403c3ec87..578554da5bd 100644 --- a/homeassistant/components/octoprint/button.py +++ b/homeassistant/components/octoprint/button.py @@ -5,7 +5,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/octoprint/camera.py b/homeassistant/components/octoprint/camera.py index 9c3049ff87d..99052993a61 100644 --- a/homeassistant/components/octoprint/camera.py +++ b/homeassistant/components/octoprint/camera.py @@ -7,7 +7,7 @@ from homeassistant.components.mjpeg.camera import MjpegCamera from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import OctoprintDataUpdateCoordinator diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 4ef78477afe..4e64a219f77 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -8,7 +8,7 @@ from omnilogic import OmniLogic, OmniLogicException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 9ec37c98d73..0572cf6fb99 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -5,7 +5,8 @@ from aiooncue import OncueDevice, OncueSensor from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 8b4cfcb61a4..4345f3498fd 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index d3fb2f22f14..6e134fd8466 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo @dataclass diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index f2a56e513f2..a6eddece5c6 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -7,7 +7,8 @@ from typing import Any from pyownet import protocol -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from .const import READ_MODE_BOOL, READ_MODE_INT diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index a412f87deaa..d0e2a0f1706 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import ( DEVICE_SUPPORT, diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 3b0f1efab38..8771ae7a701 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -1,8 +1,8 @@ """Base classes for ONVIF entities.""" from __future__ import annotations -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN from .device import ONVIFDevice diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index b23abb54f8b..b1785ed0ef5 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -7,8 +7,7 @@ from homeassistant.components.weather import Forecast, WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index f73f78cb4e8..c7806fd90d8 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -5,8 +5,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index dec0d1daae8..678f43afb6e 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -2,8 +2,8 @@ from __future__ import annotations from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, OpenGarageDataUpdateCoordinator diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 77ab0ac0aaf..1e3654958ab 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -23,7 +23,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_PIN_INDEX, DOMAIN, SERVICE_INVOKE_PIN diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py index 54c2d16fb2b..9013e50030f 100644 --- a/homeassistant/components/openhome/update.py +++ b/homeassistant/components/openhome/update.py @@ -16,7 +16,7 @@ from homeassistant.components.update import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 1a4247992a7..2501d00c2eb 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -7,8 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 66223996180..b34239c933a 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -25,8 +25,9 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index d1f99461b22..b219969e71a 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -7,8 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 2cfdd2456ab..232664d5b6b 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -21,8 +21,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 30f98bb39d1..631a4cceb0b 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -27,8 +27,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index ad94d8cafb6..6be74deaebf 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfVolume from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index fa531410e33..3c0170e543f 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -6,7 +6,8 @@ from typing import cast from pyoverkiz.enums import OverkizAttribute, OverkizState from pyoverkiz.models import Device -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index b5296d772df..f56643e8cd4 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -29,7 +29,7 @@ from homeassistant.const import ( UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 92d9aa118f0..1a871e99023 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -13,8 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index ba0beb40cf8..e2053868cb9 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 21a878fa187..17fba104c7a 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -20,8 +20,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 5e2ed77233b..a159c47a7c9 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -18,7 +18,7 @@ from homeassistant.components.media_player import ( 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index 717ee090612..b0512ededce 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -8,7 +8,7 @@ from homeassistant.components.remote import RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/pegel_online/entity.py b/homeassistant/components/pegel_online/entity.py index c8a01623c7d..e9c4ebdb909 100644 --- a/homeassistant/components/pegel_online/entity.py +++ b/homeassistant/components/pegel_online/entity.py @@ -1,7 +1,7 @@ """The PEGELONLINE base entity.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 6f72f31ae8f..969c6c7b837 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ab289c004e1..c57b969ce9c 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 5e2e507e450..d4582afa3b2 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -14,8 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CURRENCY_EURO from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index 8bdb7848bb1..755ff8d2ae7 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -2,6 +2,7 @@ from pyplaato.models.device import PlaatoDevice from homeassistant.helpers import entity +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -56,10 +57,10 @@ class PlaatoEntity(entity.Entity): return f"{self._device_id}_{self._sensor_type}" @property - def device_info(self) -> entity.DeviceInfo: + def device_info(self) -> DeviceInfo: """Get device info.""" sw_version = self._sensor_data.firmware_version - return entity.DeviceInfo( + return DeviceInfo( identifiers={(DOMAIN, self._device_id)}, manufacturer="Plaato", model=self._device_type, diff --git a/homeassistant/components/plex/button.py b/homeassistant/components/plex/button.py index 35073413037..58e0b78560b 100644 --- a/homeassistant/components/plex/button.py +++ b/homeassistant/components/plex/button.py @@ -5,8 +5,8 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 6585c011c2d..23f2895fd51 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -21,12 +21,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo 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.network import is_internal_request diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 3b66fe0cf6d..a705d11cb41 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -10,8 +10,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index c0f38cf6d5c..1c9149fad72 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -7,8 +7,8 @@ from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_ZIGBEE, + DeviceInfo, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index ac0dd0c919c..2c1f7daa880 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 627736f605d..2030483d9cd 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -20,11 +20,12 @@ 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.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index bfffb934407..c2a904ec2a1 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -16,8 +16,8 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MinutPointClient diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 1b42215483d..db1f5997e3e 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -1,6 +1,6 @@ """The Tesla Powerwall integration base entity.""" -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py index b05a5f245ff..8d1f087bfff 100644 --- a/homeassistant/components/prosegur/alarm_control_panel.py +++ b/homeassistant/components/prosegur/alarm_control_panel.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/prosegur/camera.py b/homeassistant/components/prosegur/camera.py index 9041a6526fb..bdd265d1e42 100644 --- a/homeassistant/components/prosegur/camera.py +++ b/homeassistant/components/prosegur/camera.py @@ -10,7 +10,7 @@ from pyprosegur.installation import Camera as InstallationCamera, Installation from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 70853623f0e..59708d76097 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 42bc15cf0ca..f14ef6ce2aa 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -26,7 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.json import JsonObjectType diff --git a/homeassistant/components/pure_energie/sensor.py b/homeassistant/components/pure_energie/sensor.py index 9f67665d66c..4ab77fa7893 100644 --- a/homeassistant/components/pure_energie/sensor.py +++ b/homeassistant/components/pure_energie/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/purpleair/__init__.py b/homeassistant/components/purpleair/__init__.py index f5c4090dc87..6b998f6879e 100644 --- a/homeassistant/components/purpleair/__init__.py +++ b/homeassistant/components/purpleair/__init__.py @@ -9,7 +9,7 @@ from aiopurpleair.models.sensors import SensorModel from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SHOW_ON_MAP, DOMAIN diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 84d2998e992..2f2a1d066f3 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -5,9 +5,8 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .api import PushBulletNotificationProvider diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index b681678b098..bcf869d3bba 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -20,7 +20,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 56b77dec401..73881d16a4b 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -14,8 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, UnitOfEnergy from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_change from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index febd4b61ebb..4bf410c7f87 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -31,7 +31,7 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index 38e45457462..c1af235bfc3 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -18,8 +18,8 @@ from aioqsw.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import MANUFACTURER diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index a109c4b99f7..fc0dc1f1aae 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -1,7 +1,8 @@ """Adapter to wrap the rachiopy api for home assistant.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DEFAULT_NAME, DOMAIN from .device import RachioIro diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index 3584d5242b6..c7f31a999e7 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -16,8 +16,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/radiotherm/entity.py b/homeassistant/components/radiotherm/entity.py index 7eb14548ada..384c97cac2c 100644 --- a/homeassistant/components/radiotherm/entity.py +++ b/homeassistant/components/radiotherm/entity.py @@ -4,7 +4,7 @@ from abc import abstractmethod from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import RadioThermUpdateCoordinator diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index d76ac78f7e9..6e462603dbb 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -12,7 +12,7 @@ from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiExceptio from pyrainbird.data import ModelAndVersion from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 3e2a3115e29..3b945b31db5 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -9,7 +9,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index a7fd27a051f..113cfceb7d6 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 2b3f642dfe4..ef2713cc192 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed from homeassistant.util.dt import as_timestamp, utcnow from homeassistant.util.network import is_ip_address diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 9d895f35eb7..16a93485b36 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -13,8 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index 2c324ca7093..f330ac16b8e 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -14,8 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/recollect_waste/entity.py b/homeassistant/components/recollect_waste/entity.py index 41781b10355..5ccd65cc55a 100644 --- a/homeassistant/components/recollect_waste/entity.py +++ b/homeassistant/components/recollect_waste/entity.py @@ -2,8 +2,7 @@ from aiorecollect.client import PickupEvent from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 30e251dd30b..6dd0dc2611e 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -15,7 +15,7 @@ from renault_api.renault_vehicle import RenaultVehicle from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN from .coordinator import RenaultDataUpdateCoordinator diff --git a/homeassistant/components/renson/entity.py b/homeassistant/components/renson/entity.py index 526077d2d7f..245b55d6611 100644 --- a/homeassistant/components/renson/entity.py +++ b/homeassistant/components/renson/entity.py @@ -9,7 +9,7 @@ from renson_endura_delta.field_enum import ( ) from renson_endura_delta.renson import RensonVentilation -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RensonCoordinator diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 48652eac21a..e7d62c9705a 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -5,8 +5,7 @@ from typing import TypeVar from reolink_aio.api import DUAL_LENS_MODELS -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 3544abcfdd1..e8d20ef9c10 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -25,11 +25,12 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/ridwell/entity.py b/homeassistant/components/ridwell/entity.py index 9c7ceee7f56..095ecc3c5c6 100644 --- a/homeassistant/components/ridwell/entity.py +++ b/homeassistant/components/ridwell/entity.py @@ -5,8 +5,7 @@ from datetime import date from aioridwell.model import RidwellAccount, RidwellPickupEvent -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 5fc438c2390..2b345b3b703 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -1,6 +1,7 @@ """Base class for Ring entity.""" from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from . import ATTRIBUTION, DOMAIN diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 116e022d216..5b2d85b2bca 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -24,7 +24,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LocalData, RiscoDataUpdateCoordinator, is_local diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index a4ac260887c..3a2c50e20af 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -5,8 +5,9 @@ from typing import Any from pyrisco.common import Zone +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RiscoDataUpdateCoordinator, zone_update_signal diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 713c3905f05..83564f40488 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -1,7 +1,8 @@ """Base class for Rituals Perfume Genie diffuser entity.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 6ba6f3915ec..0a9f42887a6 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -11,7 +11,7 @@ from roborock.local_api import RoborockLocalClient from roborock.roborock_typing import DeviceProp from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index c40e47ada99..27f25208a4e 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -9,7 +9,8 @@ from roborock.exceptions import RoborockException from roborock.roborock_typing import RoborockCommand from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RoborockDataUpdateCoordinator diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index a85024f8220..b6343d0dae1 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -1,8 +1,8 @@ """Base Entity for Roku.""" from __future__ import annotations -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RokuDataUpdateCoordinator diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 5dbd1e986f3..8b909392250 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -15,7 +15,8 @@ from homeassistant.components.vacuum import ( ) from homeassistant.const import STATE_IDLE, STATE_PAUSED import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 6d096ea8b1a..d56bacd67c4 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -19,11 +19,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.device_registry import DeviceInfo 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.util import convert from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/rympro/sensor.py b/homeassistant/components/rympro/sensor.py index 2c1a3ecee11..35e4b155b28 100644 --- a/homeassistant/components/rympro/sensor.py +++ b/homeassistant/components/rympro/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfVolume from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 1d85fcf0243..8edc579b7ab 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -12,9 +12,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate, UnitOfInformation from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, SIGNAL_SABNZBD_UPDATED diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index 4d5ea3d5fab..e0ecbaac024 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -6,7 +6,8 @@ from typing import cast from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_MODEL, CONF_NAME from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .bridge import SamsungTVBridge from .const import CONF_MANUFACTURER, DOMAIN diff --git a/homeassistant/components/schlage/entity.py b/homeassistant/components/schlage/entity.py index ed02269fb32..61bdbcb7730 100644 --- a/homeassistant/components/schlage/entity.py +++ b/homeassistant/components/schlage/entity.py @@ -2,7 +2,7 @@ from pyschlage.lock import Lock -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index f2c186be9e6..197f2e003d8 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -24,8 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ( diff --git a/homeassistant/components/screenlogic/entity.py b/homeassistant/components/screenlogic/entity.py index eb006b55367..955b73262a1 100644 --- a/homeassistant/components/screenlogic/entity.py +++ b/homeassistant/components/screenlogic/entity.py @@ -9,7 +9,7 @@ from screenlogicpy.const import CODE, DATA as SL_DATA, EQUIPMENT, ON_OFF from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ScreenlogicDataUpdateCoordinator diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 4d78e60db0f..cfca3c1f9ea 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -9,8 +9,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index d6679d80f69..5440372cbc8 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -13,8 +13,8 @@ from homeassistant.const import ( UnitOfPower, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 3696f618fd7..9fdd1ef9f21 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -8,8 +8,7 @@ import async_timeout from pysensibo.model import MotionSensor, SensiboDevice from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index 0c49368001d..a94941ac642 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -14,7 +14,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/sfr_box/binary_sensor.py b/homeassistant/components/sfr_box/binary_sensor.py index 9e8201bc1b5..79533576efb 100644 --- a/homeassistant/components/sfr_box/binary_sensor.py +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py index 13a1563034f..c9418bcc2e9 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index c01d298daff..1c4540b1c74 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -20,7 +20,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index ca24212a96c..8c6c4a9197a 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -17,7 +17,7 @@ from homeassistant.components.vacuum import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index ac01033f2c7..edc33c9a8a0 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -14,8 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 04c211a98cb..a9712e62d25 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -21,8 +21,7 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import ( RegistryEntry, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 548428c444c..1dc7573b738 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -11,8 +11,8 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import ( RegistryEntry, diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index 7ca48bdc46e..a947f9e177b 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -10,8 +10,9 @@ from pysiaalarm import SIAEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT from homeassistant.core import CALLBACK_TYPE, State, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index dec1b35d346..7b57fa1fc32 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -66,11 +66,11 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index 29c7167b02b..2d596ec8aac 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -5,7 +5,8 @@ from aioskybell import SkybellDevice from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index 076dcf7e590..47ee07a7004 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -12,8 +12,8 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import ConfigType from .const import ( diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index e6eeaa98c22..38d8eb32051 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -6,7 +6,8 @@ from asyncsleepiq import SleepIQBed, SleepIQSleeper from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ENTITY_TYPES, ICON_OCCUPIED diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 9bd9f7668c8..1bf3c57fee2 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -20,7 +20,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 13f402b53c3..419fd6aa8ed 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( 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.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 2987d2648c2..dbcc1931e58 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index 88d46e3689d..71bbaa472ae 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index d39f173d76b..4228f57ea46 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfElectricPotential, UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index 828e4a68121..1928e717f22 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -4,7 +4,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 6606352ffc8..4e694556598 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -18,11 +18,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 0c935d77b5d..7f2a739c26e 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -1,7 +1,7 @@ """Base classes for SmartTub entities.""" import smarttub -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 2945f890df2..e62d236c819 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -56,8 +56,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.util import Throttle, slugify diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index 0ad727faf2c..d4c45b83d82 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 936dc998c86..cd8304a1198 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import ( UnitOfPower, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import as_local diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index fd0db1be054..eee74c1007f 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -26,7 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index a929bd24b25..aa948703118 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -10,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .const import API, DOMAIN, HOST, PORT diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index 43c9ca63bb5..c4c506401d9 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -6,7 +6,7 @@ from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index e8a65239be7..d73b9d852c8 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -1,8 +1,8 @@ """Base Entity for Sonarr.""" from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index bc5e15ba989..bc096d23437 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -30,7 +30,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_platform, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 0b51687a465..90cadcdad37 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -8,8 +8,9 @@ import logging from soco.core import SoCo import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DATA_SONOS, DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED from .exception import SonosUpdateError diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f8670074c5c..63e5a551745 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -22,8 +22,11 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index a5ccb78baed..5bcf178f396 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -13,8 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 2769d045c0b..1498c4b0039 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -9,7 +9,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index 5b326db1e45..bce437437c6 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 28bbf0fcc18..508dcee9d73 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -4,7 +4,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 41d27b68672..d05e4282edf 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -25,8 +25,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index aecc34d7009..f750b364106 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -38,8 +38,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ( diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 920c9214aec..b6a6ae4a953 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -9,7 +9,7 @@ from starline import StarlineApi, StarlineDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .const import ( diff --git a/homeassistant/components/starlink/entity.py b/homeassistant/components/starlink/entity.py index 29ef9ba9f08..b726beeef0d 100644 --- a/homeassistant/components/starlink/entity.py +++ b/homeassistant/components/starlink/entity.py @@ -1,7 +1,8 @@ """Contains base entity classes for Starlink entities.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/steam_online/entity.py b/homeassistant/components/steam_online/entity.py index 364f2e72328..8ad6bd8c713 100644 --- a/homeassistant/components/steam_online/entity.py +++ b/homeassistant/components/steam_online/entity.py @@ -1,6 +1,5 @@ """Entity classes for the Steam integration.""" -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/steamist/entity.py b/homeassistant/components/steamist/entity.py index 4692b48d314..94b3d32eaa4 100644 --- a/homeassistant/components/steamist/entity.py +++ b/homeassistant/components/steamist/entity.py @@ -6,7 +6,8 @@ from aiosteamist import SteamistStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import SteamistDataUpdateCoordinator diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 1d074bba9c2..0ee087a779e 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -11,8 +11,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_PROVINCE, DOMAIN diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py index 5b0bc4d4c63..312f8bdd02d 100644 --- a/homeassistant/components/stookwijzer/sensor.py +++ b/homeassistant/components/stookwijzer/sensor.py @@ -8,8 +8,7 @@ from stookwijzer import Stookwijzer from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, StookwijzerState diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 49ad3cf0d98..091a281defc 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_US from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( diff --git a/homeassistant/components/sun/sensor.py b/homeassistant/components/sun/sensor.py index 344e0c2179e..6eccbc93d37 100644 --- a/homeassistant/components/sun/sensor.py +++ b/homeassistant/components/sun/sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEGREE, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/surepetcare/entity.py b/homeassistant/components/surepetcare/entity.py index 75d7f4e1c30..e6a44d5bfa9 100644 --- a/homeassistant/components/surepetcare/entity.py +++ b/homeassistant/components/surepetcare/entity.py @@ -6,7 +6,7 @@ from abc import abstractmethod from surepy.entities import SurepyEntity from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SurePetcareDataCoordinator diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 3718c4ebe99..52d58157e34 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -14,7 +14,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 4f6a056202c..6aae5adb3d6 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -5,7 +5,7 @@ from typing import Generic, TypeVar, cast from switchbee import SWITCHBEE_BRAND from switchbee.device import DeviceType, SwitchBeeBaseDevice -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index c0e7a51170a..cf7f97a2692 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -13,7 +13,8 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( from homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, ToggleEntity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import ToggleEntity from .const import MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index ec2f4c0bc90..d6174920ece 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -20,8 +20,8 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index be966d67eef..32877f42163 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -30,8 +30,8 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index 1d72184ad4d..78a722f262c 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -18,8 +18,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index caed3c3c320..95ea92e62ab 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -18,8 +18,8 @@ from homeassistant.helpers import ( device_registry as dr, entity_platform, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index 33fcb93182e..0551ae29d2c 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -5,9 +5,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 66ed72a3fc9..f5e23ea25ad 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import ( 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 2ec6deccf85..c2ad159fb21 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 05e1a57aaf6..d62f816b29e 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -14,7 +14,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SynoApi diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 425475dc0d0..b76699631cb 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -17,8 +17,8 @@ from homeassistant.components.camera import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SynoApi diff --git a/homeassistant/components/synology_dsm/entity.py b/homeassistant/components/synology_dsm/entity.py index 0865686ef20..bb668e292cc 100644 --- a/homeassistant/components/synology_dsm/entity.py +++ b/homeassistant/components/synology_dsm/entity.py @@ -4,7 +4,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any, TypeVar -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .common import SynoApi diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 208d299cc2e..074a423c53d 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -10,7 +10,7 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SynoApi diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 05e607d56ed..29b127bf8db 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MODULES diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 5e3065bfb53..cfc9e5b1e6e 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -1,5 +1,6 @@ """Base class for Tado entity.""" -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DEFAULT_NAME, DOMAIN, TADO_HOME, TADO_ZONE diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index abc4c4ca399..3d0a8e30727 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -6,8 +6,8 @@ from tailscale import Device as TailscaleDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 3ffa2ff4576..f1dbc26fc3a 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -13,8 +13,7 @@ from homeassistant.const import ATTR_ID, CONF_API_KEY, CONF_SHOW_ON_MAP, Platfor from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index bfa6d01032b..859b11ebd4c 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -16,9 +16,9 @@ from homeassistant.components.mqtt import ( is_connected as mqtt_connected, ) from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .discovery import ( TASMOTA_DISCOVERY_ENTITY_UPDATED, diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index a90d78380b4..b7e62846ac1 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -7,8 +7,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index db32d41cede..ce9c5222fd5 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -12,8 +12,9 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, ) from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import SIGNAL_UPDATE_ENTITY diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index 2c2d0ca154b..41403ab84f2 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform 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.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index a6621c096c3..3e702f0ebdb 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -27,7 +27,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 996490282d5..2694ef50e3a 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -37,8 +37,10 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import ( + DeviceInfo, + async_get as async_get_dev_reg, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.helpers.typing import StateType diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index bb894753fb8..f0cf94bb825 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -13,7 +13,7 @@ from tololib.message_info import SettingsInfo, StatusInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index ce5ec4191c5..6d1b84ec5d7 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -26,8 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index e893bbf9e2c..75e3ddb0370 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 186f6805b7f..b89df6c9c25 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4bf076a59bc..890793b898d 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -7,7 +7,7 @@ from typing import Any, Concatenate, ParamSpec, TypeVar from kasa import SmartDevice from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/tplink_omada/entity.py b/homeassistant/components/tplink_omada/entity.py index 41cb1c69180..bb330ef417a 100644 --- a/homeassistant/components/tplink_omada/entity.py +++ b/homeassistant/components/tplink_omada/entity.py @@ -4,7 +4,7 @@ from typing import Generic, TypeVar from tplink_omada_client.devices import OmadaDevice from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index ad31f20e3cf..d15669745ef 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -38,8 +38,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index 712f8eda75a..d142fe69db5 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index c7154c19f15..d186e19a2c8 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -11,7 +11,7 @@ from pytradfri.device import Device from pytradfri.error import RequestError from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LOGGER diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index 366c193f8fe..a673f624a47 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -14,8 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index b5f993073a5..a5e76299b61 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -14,8 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index f34eae3cf1f..3ec7d137b6e 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -20,8 +20,7 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 833c1910d4e..5c5e530ccbf 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -9,9 +9,8 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, UnitOfDataRate from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TransmissionClient diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 89f89e079fa..34d8de5d620 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -6,9 +6,8 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, SWITCH_TYPES diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 998e5a55e63..3aae417aac7 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -9,8 +9,9 @@ from typing import Any, Literal, Self, overload from tuya_iot import TuyaDevice, TuyaDeviceManager +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY, DPCode, DPType from .util import remap_value diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 90cf4266ae6..289e319df1b 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -8,7 +8,7 @@ from tuya_iot import TuyaHomeManager, TuyaScene from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py index 5a1a1758d3e..5c1d71fa03b 100644 --- a/homeassistant/components/twentemilieu/entity.py +++ b/homeassistant/components/twentemilieu/entity.py @@ -7,8 +7,8 @@ from twentemilieu import WasteType from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 2a071bb6966..5ddd22c8a23 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -22,7 +22,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index eb83fe490e7..f5479917064 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -9,8 +9,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 7d9373d1188..05ad2f56a8c 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -21,9 +21,10 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceEntryType, + DeviceInfo, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity import Entity, EntityDescription from .const import ATTR_MANUFACTURER, DOMAIN diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f931bd06e1c..ae339eb8d22 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -41,8 +41,7 @@ from homeassistant.components.switch import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index a8a4c78465d..d42e611be7e 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -22,7 +22,8 @@ from pyunifiprotect.data import ( from homeassistant.core import callback import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import UNDEFINED from .const import ( diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index 0b3926f813f..4e1b003a504 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -4,7 +4,8 @@ import upb_lib from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_COMMAND, CONF_FILE_PATH, CONF_HOST, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import ( ATTR_ADDRESS, diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index cd356925de1..283842adaaa 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -21,12 +21,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/upnp/entity.py b/homeassistant/components/upnp/entity.py index a3d7709a5d5..e53d89018fb 100644 --- a/homeassistant/components/upnp/entity.py +++ b/homeassistant/components/upnp/entity.py @@ -3,7 +3,8 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import UpnpDataUpdateCoordinator diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 56d570110b0..55faf7ccb3a 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -4,8 +4,7 @@ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index d5caf36fa18..3057bd7c220 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -3,8 +3,8 @@ from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import UptimeRobotDataUpdateCoordinator diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index 01e554b1666..64b271d4200 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 7301158d6c6..f3e86136f5d 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -32,8 +32,8 @@ from homeassistant.helpers import ( entity_platform, entity_registry as er, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 473b9fa07d1..1feda8e694a 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -21,7 +21,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/velbus/entity.py b/homeassistant/components/velbus/entity.py index 46d9f03b4fb..45220e1a9b4 100644 --- a/homeassistant/components/velbus/entity.py +++ b/homeassistant/components/velbus/entity.py @@ -8,7 +8,8 @@ from typing import Any, Concatenate, ParamSpec, TypeVar from velbusaio.channels import Channel as VelbusChannel from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 4b2d2955832..3bf74f57413 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import update_coordinator -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_SLEEP, VENSTAR_TIMEOUT diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 284b8d6b00a..26e74cceb9e 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -11,7 +11,7 @@ from homeassistant.components.alarm_control_panel import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ALARM_ARMING, STATE_ALARM_DISARMING from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 68d549eaa5d..cadb9b6788d 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -8,7 +8,8 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LAST_TRIP_TIME, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 90ad926aeb7..c9d98041a2c 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -10,7 +10,7 @@ from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 6af64060ab5..94a27784e78 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -10,7 +10,7 @@ from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index a4f4d1b4e43..0fb16aa87c4 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -9,7 +9,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 6c3dcd81295..427ca5e6ea8 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -7,7 +7,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/version/entity.py b/homeassistant/components/version/entity.py index d950c6394b8..0ac1d834aac 100644 --- a/homeassistant/components/version/entity.py +++ b/homeassistant/components/version/entity.py @@ -1,7 +1,7 @@ """Common entity class for Version integration.""" -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, HOME_ASSISTANT diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 8e6ad545bd0..0e01a593021 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -4,7 +4,8 @@ from typing import Any from pyvesync.vesyncbasedevice import VeSyncBaseDevice -from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, ToggleEntity from .const import DOMAIN, VS_FANS, VS_LIGHTS, VS_SENSORS, VS_SWITCHES diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 385b64e845f..89e8bec42d1 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -19,7 +19,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ViCareRequiredKeysMixin diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index c0e7117a74c..ac025ff37d1 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -16,7 +16,7 @@ from homeassistant.components.button import ButtonEntity, ButtonEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ViCareRequiredKeysMixinWithSet diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 9a55da0f219..d5beff4b268 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 43edf1a0cef..a4b9e9d7f92 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -30,7 +30,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ViCareRequiredKeysMixin diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 59ed07bdeb2..c0d77dd46b6 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -22,7 +22,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index a989cea488f..057fd33e8dc 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -26,11 +26,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo 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 . import VizioAppsDataUpdateCoordinator diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 14728c05e53..87bc158331e 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -21,8 +21,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/voip/entity.py b/homeassistant/components/voip/entity.py index 9b3cc641a66..9e1e067b195 100644 --- a/homeassistant/components/voip/entity.py +++ b/homeassistant/components/voip/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.helpers import entity +from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN from .devices import VoIPDevice @@ -18,6 +19,6 @@ class VoIPEntity(entity.Entity): """Initialize VoIP entity.""" self._device = device self._attr_unique_id = f"{device.voip_id}-{self.entity_description.key}" - self._attr_device_info = entity.DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.voip_id)}, ) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 880d02cfeae..d207e36e3c9 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -19,7 +19,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index b943240167a..06f8d0ad5a2 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -17,8 +17,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py index 791ae9ee7c4..20c8ff78432 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -16,8 +16,8 @@ from homeassistant.components.calendar import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, generate_entity_id +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index b5e935c27f1..9b27b9c4bd1 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index cf709805f6d..2a620e48937 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -21,8 +21,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.location import find_coordinates from homeassistant.util.unit_conversion import DistanceConverter diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 579c97c5277..11903ebdd68 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -32,8 +32,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.trigger import PluggableAction diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 1debc32a39b..cbb2f31c79d 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -7,7 +7,7 @@ import logging from pywemo.exceptions import ActionException -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .wemo_device import DeviceCoordinator diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index fb01d117c08..0205a10521d 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -16,8 +16,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index c85bc9fd473..110943a6503 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -24,9 +24,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import ( CONNECTION_UPNP, + DeviceInfo, async_get as async_get_device_registry, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index d1c5d6cf8f8..2d38d713859 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -26,7 +26,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo, generate_entity_id +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import WhirlpoolData diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index f761badfa2b..c3cad90e045 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -17,7 +17,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 6333139e540..5163e0b3a6e 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -16,8 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DOMAIN, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index a802535441a..11ef186ba15 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -10,11 +10,12 @@ from homeassistant.const import CONF_PORT, CONF_TIMEOUT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 58ba237ae68..067197c8a14 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -8,7 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .parent_device import WiLightParent diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index 67608db157a..87c3171d836 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -8,8 +8,8 @@ from pywizlight.bulblibrary import BulbType from homeassistant.const import ATTR_HW_VERSION, ATTR_MODEL from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index 2bdd2e46e2c..81405190228 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -1,6 +1,5 @@ """Models for WLED.""" -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index c80608ab1c2..d1666fa9097 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -16,8 +16,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index 0bf58c249ae..b5c87fbc0f3 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -8,7 +8,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 8d0016590ed..ffbbee8637d 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -3,8 +3,7 @@ from __future__ import annotations from yarl import URL -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PresenceData, XboxUpdateCoordinator diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index ab16afa9280..060720338e8 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -22,7 +22,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 75595483608..fdb4e80cf9e 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -22,7 +22,7 @@ from homeassistant.components.remote import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index ef36bd67778..8f5ac19ee68 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -23,8 +23,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index b5057a4a3dd..e92dd76be39 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_GATEWAY, DOMAIN diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 81ca71d6b68..da860c7045e 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -10,7 +10,8 @@ from miio import Device, DeviceException from homeassistant.const import ATTR_CONNECTIONS, CONF_MODEL from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 655a04a4340..e1b3aee9ff4 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -7,7 +7,8 @@ from micloud.micloudexception import MiCloudAccessDenied from miio import DeviceException, gateway from miio.gateway.gateway import GATEWAY_MODEL_EU -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 9b8357a534f..0a4ed1527c0 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -36,7 +36,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import color, dt as dt_util diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 86c7905848a..17d60e1a952 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -42,7 +42,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index 86b5839b51f..179e20d509d 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -1,8 +1,8 @@ """Base class for yale_smart_alarm entity.""" from homeassistant.const import CONF_NAME, CONF_USERNAME -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, MODEL diff --git a/homeassistant/components/yalexs_ble/entity.py b/homeassistant/components/yalexs_ble/entity.py index 51f30b8a861..9135f0c0896 100644 --- a/homeassistant/components/yalexs_ble/entity.py +++ b/homeassistant/components/yalexs_ble/entity.py @@ -6,7 +6,8 @@ from yalexs_ble import ConnectionInfo, LockInfo, LockState from homeassistant.components import bluetooth from homeassistant.core import callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN from .models import YaleXSBLEData diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 639f0b69a41..c3851074365 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -13,8 +13,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, diff --git a/homeassistant/components/yeelight/entity.py b/homeassistant/components/yeelight/entity.py index 9422ec9980d..8056ea085b7 100644 --- a/homeassistant/components/yeelight/entity.py +++ b/homeassistant/components/yeelight/entity.py @@ -2,7 +2,8 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity from .const import DOMAIN from .device import YeelightDevice diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 09da5545d57..0221bd94a7e 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -9,7 +9,7 @@ from yolink.exception import YoLinkAuthFailError, YoLinkClientError from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 057533081e6..36175ae9cf3 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/youtube/entity.py b/homeassistant/components/youtube/entity.py index 46deaf40450..6f7f0b28dd2 100644 --- a/homeassistant/components/youtube/entity.py +++ b/homeassistant/components/youtube/entity.py @@ -1,8 +1,8 @@ """Entity representing a YouTube account.""" from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_TITLE, DOMAIN, MANUFACTURER diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index f3e49447056..31275dd908d 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -21,8 +21,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index f94f9ca8a3a..ff98496bd40 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -10,8 +10,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 41ecb751b86..884f87d36f6 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util diff --git a/homeassistant/components/zeversolar/entity.py b/homeassistant/components/zeversolar/entity.py index ccda0add910..77ae5ee61f8 100644 --- a/homeassistant/components/zeversolar/entity.py +++ b/homeassistant/components/zeversolar/entity.py @@ -1,7 +1,7 @@ """Base Entity for Zeversolar sensors.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 02c16930d53..1f3a71f4cbf 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -27,8 +27,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from . import discovery diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 885cd788f70..04c74a44dbe 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -8,8 +8,8 @@ from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 7f34629400f..f2b16a37834 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -11,7 +11,7 @@ from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import entity from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -79,11 +79,11 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._extra_state_attributes @property - def device_info(self) -> entity.DeviceInfo: + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" zha_device_info = self._zha_device.device_info ieee = zha_device_info["ieee"] - return entity.DeviceInfo( + return DeviceInfo( connections={(CONNECTION_ZIGBEE, ieee)}, identifiers={(DOMAIN, ieee)}, manufacturer=zha_device_info[ATTR_MANUFACTURER], diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index d9b306da4dd..2e79f3804ab 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -4,8 +4,7 @@ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_local, utcnow diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 7017254034a..0b9c68e9664 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -16,8 +16,9 @@ from zwave_js_server.model.value import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import UNDEFINED from .const import DOMAIN, LOGGER diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index adce141f91c..3b1faa40fa8 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -31,7 +31,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.typing import ConfigType from .const import ( diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index 86cebe81180..e35f55d6fda 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -8,8 +8,9 @@ from homeassistant.const import CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DOMAIN, PLATFORMS, ZWaveMePlatform diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 4dd9233c6ab..d942680e490 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -28,7 +28,6 @@ if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from . import entity_registry - from .entity import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -65,6 +64,26 @@ DISABLED_CONFIG_ENTRY = DeviceEntryDisabler.CONFIG_ENTRY.value DISABLED_INTEGRATION = DeviceEntryDisabler.INTEGRATION.value DISABLED_USER = DeviceEntryDisabler.USER.value + +class DeviceInfo(TypedDict, total=False): + """Entity device information for device registry.""" + + configuration_url: str | URL | None + connections: set[tuple[str, str]] + default_manufacturer: str + default_model: str + default_name: str + entry_type: DeviceEntryType | None + identifiers: set[tuple[str, str]] + manufacturer: str | None + model: str | None + name: str | None + suggested_area: str | None + sw_version: str | None + hw_version: str | None + via_device: tuple[str, str] + + DEVICE_INFO_TYPES = { # Device info is categorized by finding the first device info type which has all # the keys of the device info. The link device info type must be kept first diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7d240cc0320..9338346fc8b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -12,10 +12,9 @@ import logging import math import sys from timeit import default_timer as timer -from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, TypeVar, final +from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar, final import voluptuous as vol -from yarl import URL from homeassistant.backports.functools import cached_property from homeassistant.config import DATA_CUSTOMIZE @@ -41,7 +40,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify from . import device_registry as dr, entity_registry as er -from .device_registry import DeviceEntryType, EventDeviceRegistryUpdatedData +from .device_registry import DeviceInfo, EventDeviceRegistryUpdatedData from .event import ( async_track_device_registry_updated_event, async_track_entity_registry_updated_event, @@ -175,25 +174,6 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: return entry.unit_of_measurement -class DeviceInfo(TypedDict, total=False): - """Entity device information for device registry.""" - - configuration_url: str | URL | None - connections: set[tuple[str, str]] - default_manufacturer: str - default_model: str - default_name: str - entry_type: DeviceEntryType | None - identifiers: set[tuple[str, str]] - manufacturer: str | None - model: str | None - name: str | None - suggested_area: str | None - sw_version: str | None - hw_version: str | None - via_device: tuple[str, str] - - ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory) diff --git a/homeassistant/helpers/sensor.py b/homeassistant/helpers/sensor.py index 96e6b83a167..0785a78850a 100644 --- a/homeassistant/helpers/sensor.py +++ b/homeassistant/helpers/sensor.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from homeassistant import const -from .entity import DeviceInfo +from .device_registry import DeviceInfo if TYPE_CHECKING: # `sensor_state_data` is a second-party library (i.e. maintained by Home Assistant diff --git a/tests/common.py b/tests/common.py index 33855d4e8da..eb8c8417f16 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1138,7 +1138,7 @@ class MockEntity(entity.Entity): return self._handle("device_class") @property - def device_info(self) -> entity.DeviceInfo | None: + def device_info(self) -> dr.DeviceInfo | None: """Info how it links to a device.""" return self._handle("device_info") diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index 29e6f9a8f31..1868d9b005e 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -17,7 +17,7 @@ from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from tests.common import MockConfigEntry, MockPlatform, mock_entity_platform diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 5906ab0bf25..d11f5cd5ccd 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -40,7 +40,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import current_entry from homeassistant.const import UnitOfTemperature from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index f0e7752d7c0..814a46f4a25 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from tests.common import MockConfigEntry diff --git a/tests/components/kostal_plenticore/test_helper.py b/tests/components/kostal_plenticore/test_helper.py index cc522c96974..61df222fd9e 100644 --- a/tests/components/kostal_plenticore/test_helper.py +++ b/tests/components/kostal_plenticore/test_helper.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.kostal_plenticore.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from tests.common import MockConfigEntry From 2e1a5ddf2bbc317e5e852fab7d9f20c4b27a396a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Aug 2023 04:09:13 +0200 Subject: [PATCH 154/179] Don't allow creating device if config entry does not exist (#98157) * Don't allow creating device if config entry does not exist * Fix test * Update test --- homeassistant/helpers/device_registry.py | 16 ++++++++-------- tests/components/mqtt/test_sensor.py | 5 ++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index d942680e490..9c2492d65e8 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -158,7 +158,7 @@ class DeviceInfoError(HomeAssistantError): def _validate_device_info( - config_entry: ConfigEntry | None, + config_entry: ConfigEntry, device_info: DeviceInfo, ) -> str: """Process a device info.""" @@ -167,7 +167,7 @@ def _validate_device_info( # If no keys or not enough info to match up, abort if not device_info.get("connections") and not device_info.get("identifiers"): raise DeviceInfoError( - config_entry.domain if config_entry else "unknown", + config_entry.domain, device_info, "device info must include at least one of identifiers or connections", ) @@ -182,7 +182,7 @@ def _validate_device_info( if device_info_type is None: raise DeviceInfoError( - config_entry.domain if config_entry else "unknown", + config_entry.domain, device_info, ( "device info needs to either describe a device, " @@ -527,6 +527,10 @@ class DeviceRegistry: device_info[key] = val # type: ignore[literal-required] config_entry = self.hass.config_entries.async_get_entry(config_entry_id) + if config_entry is None: + raise HomeAssistantError( + f"Can't link device to unknown config entry {config_entry_id}" + ) device_info_type = _validate_device_info(config_entry, device_info) if identifiers is None or identifiers is UNDEFINED: @@ -550,11 +554,7 @@ class DeviceRegistry: ) self.devices[device.id] = device # If creating a new device, default to the config entry name - if ( - device_info_type == "primary" - and (not name or name is UNDEFINED) - and config_entry - ): + if device_info_type == "primary" and (not name or name is UNDEFINED): name = config_entry.title if default_manufacturer is not UNDEFINED and device.manufacturer is None: diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 30eb0fd1939..043c8d539b6 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -66,6 +66,7 @@ from .test_common import ( ) from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache_with_extra_data, @@ -1123,9 +1124,11 @@ async def test_entity_device_info_with_hub( ) -> None: """Test MQTT sensor device registry integration.""" await mqtt_mock_entry() + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) registry = dr.async_get(hass) hub = registry.async_get_or_create( - config_entry_id="123", + config_entry_id=other_config_entry.entry_id, connections=set(), identifiers={("mqtt", "hub-id")}, manufacturer="manufacturer", From 914baaa2ba14acbc3fd2c3872d994f40857bffee Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 04:10:46 +0200 Subject: [PATCH 155/179] Migrate DirecTV to has entity name (#98159) * Migrate DirecTV to has entity name * Migrate DirecTV to has entity name --- homeassistant/components/directv/entity.py | 18 ++++++------------ .../components/directv/media_player.py | 2 +- homeassistant/components/directv/remote.py | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py index cd4017eb389..0da2cfcb9d6 100644 --- a/homeassistant/components/directv/entity.py +++ b/homeassistant/components/directv/entity.py @@ -1,8 +1,6 @@ """Base DirecTV Entity.""" from __future__ import annotations -from typing import cast - from directv import DIRECTV from homeassistant.helpers.device_registry import DeviceInfo @@ -14,23 +12,19 @@ from .const import DOMAIN class DIRECTVEntity(Entity): """Defines a base DirecTV entity.""" - def __init__(self, *, dtv: DIRECTV, address: str = "0") -> None: + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: """Initialize the DirecTV entity.""" self._address = address self._device_id = address if address != "0" else dtv.device.info.receiver_id self._is_client = address != "0" self.dtv = dtv - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this DirecTV receiver.""" - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device_id)}, manufacturer=self.dtv.device.info.brand, - # Instead of setting the device name to the entity name, directv - # should be updated to set has_entity_name = True, and set the entity - # name to None - name=cast(str | None, self.name), + name=name, sw_version=self.dtv.device.info.version, via_device=(DOMAIN, self.dtv.device.info.receiver_id), ) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 8c1570db159..63d086564ee 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -80,11 +80,11 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): """Initialize DirecTV media player.""" super().__init__( dtv=dtv, + name=name, address=address, ) self._attr_unique_id = self._device_id - self._attr_name = name self._attr_device_class = MediaPlayerDeviceClass.RECEIVER self._attr_available = False diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index c8c84a7f0cc..d100abd3495 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -49,11 +49,11 @@ class DIRECTVRemote(DIRECTVEntity, RemoteEntity): """Initialize DirecTV remote.""" super().__init__( dtv=dtv, + name=name, address=address, ) self._attr_unique_id = self._device_id - self._attr_name = name self._attr_available = False self._attr_is_on = True From 24add3f7667558f402aba18ec269007349cbbac1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 05:09:26 +0200 Subject: [PATCH 156/179] Migrate Doorbird to has entity name (#98161) --- homeassistant/components/doorbird/button.py | 4 ++-- homeassistant/components/doorbird/camera.py | 10 +++++----- homeassistant/components/doorbird/entity.py | 2 ++ homeassistant/components/doorbird/strings.json | 13 +++++++++++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/doorbird/button.py b/homeassistant/components/doorbird/button.py index ad1356023fc..fb13a6f5be3 100644 --- a/homeassistant/components/doorbird/button.py +++ b/homeassistant/components/doorbird/button.py @@ -85,9 +85,9 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity): self.entity_description = entity_description if self._relay == IR_RELAY: - self._attr_name = f"{self._doorstation.name} IR" + self._attr_name = "IR" else: - self._attr_name = f"{self._doorstation.name} Relay {self._relay}" + self._attr_name = f"Relay {self._relay}" self._attr_unique_id = f"{self._mac_addr}_{self._relay}" def press(self) -> None: diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 5983e639851..c1c8a622af8 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -49,7 +49,7 @@ async def async_setup_entry( doorstation_info, device.live_image_url, "live", - f"{doorstation.name} Live", + "live", doorstation.doorstation_events, _LIVE_INTERVAL, device.rtsp_live_video_url, @@ -59,7 +59,7 @@ async def async_setup_entry( doorstation_info, device.history_image_url(1, "doorbell"), "last_ring", - f"{doorstation.name} Last Ring", + "last_ring", [], _LAST_VISITOR_INTERVAL, ), @@ -68,7 +68,7 @@ async def async_setup_entry( doorstation_info, device.history_image_url(1, "motionsensor"), "last_motion", - f"{doorstation.name} Last Motion", + "last_motion", [], _LAST_MOTION_INTERVAL, ), @@ -85,7 +85,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): doorstation_info, url, camera_id, - name, + translation_key, doorstation_events, interval, stream_url=None, @@ -94,7 +94,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): super().__init__(doorstation, doorstation_info) self._url = url self._stream_url = stream_url - self._attr_name = name + self._attr_translation_key = translation_key self._last_image: bytes | None = None if self._stream_url: self._attr_supported_features = CameraEntityFeature.STREAM diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index ca0958af0ce..65431e38be1 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -16,6 +16,8 @@ from .util import get_mac_address_from_doorstation_info class DoorBirdEntity(Entity): """Base class for doorbird entities.""" + _attr_has_entity_name = True + def __init__(self, doorstation, doorstation_info): """Initialize the entity.""" super().__init__() diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 44fd07c405e..ceaf1a891ee 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -33,5 +33,18 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "camera": { + "live": { + "name": "live" + }, + "last_ring": { + "name": "Last ring" + }, + "last_motion": { + "name": "Last motion" + } + } } } From 9a1bfe1e1c06f0a9df1bc444f98fb50b83b272bb Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Fri, 11 Aug 2023 02:19:06 -0400 Subject: [PATCH 157/179] Bump pynws 1.5.1; fix regression for precipitation probability (#98237) bump pynws 1.5.1 --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 7f5d01f9897..05194d85a26 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["metar", "pynws"], "quality_scale": "platinum", - "requirements": ["pynws==1.5.0"] + "requirements": ["pynws==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6b88719a737..e7496d77298 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1875,7 +1875,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.5.0 +pynws==1.5.1 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb0cf0f8f5..83ad6578cee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1388,7 +1388,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.5.0 +pynws==1.5.1 # homeassistant.components.nx584 pynx584==0.5 From 25231637a5bf3da94bb4fe97fc5203c6e5c052d5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 08:27:40 +0200 Subject: [PATCH 158/179] Add device to DWD (#98120) --- homeassistant/components/dwd_weather_warnings/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 62bb4af7930..6cda8c0b304 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -144,6 +145,10 @@ class DwdWeatherWarningsSensor( self._attr_name = f"{DEFAULT_NAME} {entry.title} {description.name}" self._attr_unique_id = f"{entry.unique_id}-{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, name=f"{DEFAULT_NAME} {entry.title}" + ) + self.api = coordinator.api @property From 832a8247de4c3b890a7e4ea71bbb281c376433ad Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 11 Aug 2023 10:11:13 +0200 Subject: [PATCH 159/179] Fix CI mypy issues (#98241) Fix CI --- homeassistant/components/dwd_weather_warnings/sensor.py | 2 +- homeassistant/components/opensky/sensor.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 6cda8c0b304..7bc683d245d 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -24,7 +24,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index a890d022e0a..e6a165b36ee 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -12,8 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType From c62081430bd6fd47c3deb0c906b7aacef4f54e7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:00:55 +0200 Subject: [PATCH 160/179] Refactor JSON attribute parsing in rest (#97526) * Refactor JSON attribute parsing in rest * Early return --- homeassistant/components/rest/sensor.py | 32 +++----------------- homeassistant/components/rest/util.py | 40 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/rest/util.py diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 1a74735c670..f7743a853ad 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -5,7 +5,6 @@ import logging import ssl from typing import Any -from jsonpath import jsonpath import voluptuous as vol from homeassistant.components.sensor import ( @@ -39,13 +38,13 @@ from homeassistant.helpers.template_entity import ( ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.json import json_loads from . import async_get_config_and_coordinator, create_rest_data_from_config from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, DEFAULT_SENSOR_NAME from .data import RestData from .entity import RestEntity from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA +from .util import parse_json_attributes _LOGGER = logging.getLogger(__name__) @@ -163,32 +162,9 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity): value = self.rest.data_without_xml() if self._json_attrs: - if value: - try: - json_dict = json_loads(value) - if self._json_attrs_path is not None: - json_dict = jsonpath(json_dict, self._json_attrs_path) - # jsonpath will always store the result in json_dict[0] - # so the next line happens to work exactly as needed to - # find the result - if isinstance(json_dict, list): - json_dict = json_dict[0] - if isinstance(json_dict, dict): - attrs = { - k: json_dict[k] for k in self._json_attrs if k in json_dict - } - self._attr_extra_state_attributes = attrs - else: - _LOGGER.warning( - "JSON result was not a dictionary" - " or list with 0th element a dictionary" - ) - except ValueError: - _LOGGER.warning("REST result could not be parsed as JSON") - _LOGGER.debug("Erroneous JSON: %s", value) - - else: - _LOGGER.warning("Empty reply found when expecting JSON data") + self._attr_extra_state_attributes = parse_json_attributes( + value, self._json_attrs, self._json_attrs_path + ) raw_value = value diff --git a/homeassistant/components/rest/util.py b/homeassistant/components/rest/util.py new file mode 100644 index 00000000000..5625be3897a --- /dev/null +++ b/homeassistant/components/rest/util.py @@ -0,0 +1,40 @@ +"""Helpers for RESTful API.""" + +import logging +from typing import Any + +from jsonpath import jsonpath + +from homeassistant.util.json import json_loads + +_LOGGER = logging.getLogger(__name__) + + +def parse_json_attributes( + value: str | None, json_attrs: list[str], json_attrs_path: str | None +) -> dict[str, Any]: + """Parse JSON attributes.""" + if not value: + _LOGGER.warning("Empty reply found when expecting JSON data") + return {} + + try: + json_dict = json_loads(value) + if json_attrs_path is not None: + json_dict = jsonpath(json_dict, json_attrs_path) + # jsonpath will always store the result in json_dict[0] + # so the next line happens to work exactly as needed to + # find the result + if isinstance(json_dict, list): + json_dict = json_dict[0] + if isinstance(json_dict, dict): + return {k: json_dict[k] for k in json_attrs if k in json_dict} + + _LOGGER.warning( + "JSON result was not a dictionary or list with 0th element a dictionary" + ) + except ValueError: + _LOGGER.warning("REST result could not be parsed as JSON") + _LOGGER.debug("Erroneous JSON: %s", value) + + return {} From 41572480fd24a4356662094d3dea00c999b0ca2f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 11:58:02 +0200 Subject: [PATCH 161/179] Migrate DenonAVR to has entity name (#98155) --- homeassistant/components/denonavr/media_player.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 5674480d493..cad6656d01d 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -217,6 +217,9 @@ def async_log_errors( class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__( self, receiver: DenonAVR, @@ -225,7 +228,6 @@ class DenonDevice(MediaPlayerEntity): update_audyssey: bool, ) -> None: """Initialize the device.""" - self._attr_name = receiver.name self._attr_unique_id = unique_id assert config_entry.unique_id self._attr_device_info = DeviceInfo( @@ -234,7 +236,7 @@ class DenonDevice(MediaPlayerEntity): identifiers={(DOMAIN, config_entry.unique_id)}, manufacturer=config_entry.data[CONF_MANUFACTURER], model=config_entry.data[CONF_MODEL], - name=config_entry.title, + name=receiver.name, ) self._attr_sound_mode_list = receiver.sound_mode_list From b67e290eaacb0961e01a4724ca8064417b03afff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 12:15:04 +0200 Subject: [PATCH 162/179] Use explicit device name in Broadlink (#98229) --- homeassistant/components/broadlink/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index d42e2b76b99..796698c6a4c 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -45,6 +45,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): """Representation of a Broadlink light.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, device): """Initialize the light.""" From a0ac8ba5a6a65b8bf7c3574e50c4c7e0fce3a29e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 11 Aug 2023 03:21:19 -0700 Subject: [PATCH 163/179] Enforce a minimum temperature range for nest thermostats (#98238) --- homeassistant/components/nest/climate.py | 8 +++ tests/components/nest/test_climate.py | 69 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 02874cab84c..0dcdec1cac1 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -75,6 +75,7 @@ FAN_INV_MODES = list(FAN_INV_MODE_MAP) MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API MIN_TEMP = 10 MAX_TEMP = 32 +MIN_TEMP_RANGE = 1.66667 async def async_setup_entry( @@ -313,6 +314,13 @@ class ThermostatEntity(ClimateEntity): try: if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: if low_temp and high_temp: + if high_temp - low_temp < MIN_TEMP_RANGE: + # Ensure there is a minimum gap from the new temp. Pick + # the temp that is not changing as the one to move. + if abs(high_temp - self.target_temperature_high) < 0.01: + high_temp = low_temp + MIN_TEMP_RANGE + else: + low_temp = high_temp - MIN_TEMP_RANGE await trait.set_range(low_temp, high_temp) elif hvac_mode == HVACMode.COOL and temp: await trait.set_cool(temp) diff --git a/tests/components/nest/test_climate.py b/tests/components/nest/test_climate.py index 037894b43f5..c920eb5717d 100644 --- a/tests/components/nest/test_climate.py +++ b/tests/components/nest/test_climate.py @@ -758,6 +758,75 @@ async def test_thermostat_set_temperature_hvac_mode( } +@pytest.mark.parametrize( + ("setpoint", "target_low", "target_high", "expected_params"), + [ + ( + { + "heatCelsius": 19.0, + "coolCelsius": 25.0, + }, + 19.0, + 20.0, + # Cool is accepted and lowers heat by the min range + {"heatCelsius": 18.33333, "coolCelsius": 20.0}, + ), + ( + { + "heatCelsius": 19.0, + "coolCelsius": 25.0, + }, + 24.0, + 25.0, + # Cool is accepted and lowers heat by the min range + {"heatCelsius": 24.0, "coolCelsius": 25.66667}, + ), + ], +) +async def test_thermostat_set_temperature_range_too_close( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, + setpoint: dict[str, Any], + target_low: float, + target_high: float, + expected_params: dict[str, Any], +) -> None: + """Test setting an HVAC temperature range that is too small of a range.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEATCOOL", + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": setpoint, + }, + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVACMode.HEAT_COOL + + # Move the target temp to be in too small of a range + await common.async_set_temperature( + hass, + target_temp_low=target_low, + target_temp_high=target_high, + ) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange", + "params": expected_params, + } + + async def test_thermostat_set_heat_cool( hass: HomeAssistant, setup_platform: PlatformSetup, From 990ec1d4454b0399a05e92f4b1f019e4a85c01aa Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 11 Aug 2023 13:07:45 +0200 Subject: [PATCH 164/179] Make gardena closing sensor unavailable when closed (#98133) --- homeassistant/components/gardena_bluetooth/sensor.py | 5 +++++ .../components/gardena_bluetooth/snapshots/test_sensor.ambr | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index eaa44d9d4fb..ebc83ae88af 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -117,3 +117,8 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity): self._attr_native_value = time super()._handle_coordinator_update() return + + @property + def available(self) -> bool: + """Sensor only available when open.""" + return super().available and self._attr_native_value is not None diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr index 14135cb390c..8df37b40abc 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -35,7 +35,7 @@ 'entity_id': 'sensor.mock_title_valve_closing', 'last_changed': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- # name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery] From fb66ceb302261292ea7f58f2e3961ea0d35e3fc8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:13:04 +0200 Subject: [PATCH 165/179] Update mypy to 1.5.0 (#98179) --- homeassistant/components/bluetooth/manager.py | 2 +- homeassistant/components/litterrobot/binary_sensor.py | 4 ++-- homeassistant/components/litterrobot/select.py | 2 +- homeassistant/components/litterrobot/sensor.py | 2 +- mypy.ini | 2 +- requirements_test.txt | 2 +- script/hassfest/mypy_config.py | 5 +++-- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index ce778e0309b..bd91c622316 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -637,7 +637,7 @@ class BluetoothManager: else: # We could write out every item in the typed dict here # but that would be a bit inefficient and verbose. - callback_matcher.update(matcher) # type: ignore[typeddict-item] + callback_matcher.update(matcher) callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True) connectable = callback_matcher[CONNECTABLE] diff --git a/homeassistant/components/litterrobot/binary_sensor.py b/homeassistant/components/litterrobot/binary_sensor.py index 5308a3b4f83..0872c5c831d 100644 --- a/homeassistant/components/litterrobot/binary_sensor.py +++ b/homeassistant/components/litterrobot/binary_sensor.py @@ -48,7 +48,7 @@ class LitterRobotBinarySensorEntity(LitterRobotEntity[_RobotT], BinarySensorEnti BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, ...]] = { - LitterRobot: ( + LitterRobot: ( # type: ignore[type-abstract] # only used for isinstance check RobotBinarySensorEntityDescription[LitterRobot]( key="sleeping", translation_key="sleeping", @@ -66,7 +66,7 @@ BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, . is_on_fn=lambda robot: robot.sleep_mode_enabled, ), ), - Robot: ( + Robot: ( # type: ignore[type-abstract] # only used for isinstance check RobotBinarySensorEntityDescription[Robot]( key="power_status", translation_key="power_status", diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 6fabd6ea526..7f2ea62f956 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -48,7 +48,7 @@ class RobotSelectEntityDescription( ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = { - LitterRobot: RobotSelectEntityDescription[LitterRobot, int]( + LitterRobot: RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check key="cycle_delay", translation_key="cycle_delay", icon="mdi:timer-outline", diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index ba601a0ba54..935bbaca595 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -66,7 +66,7 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { - LitterRobot: [ + LitterRobot: [ # type: ignore[type-abstract] # only used for isinstance check RobotSensorEntityDescription[LitterRobot]( key="waste_drawer_level", translation_key="waste_drawer", diff --git a/mypy.ini b/mypy.ini index b3ab53bf8a9..1c47ad019a2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,7 @@ warn_unused_configs = true warn_unused_ignores = true enable_error_code = ignore-without-code, redundant-self, truthy-iterable disable_error_code = annotation-unchecked -strict_concatenate = false +extra_checks = false check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/requirements_test.txt b/requirements_test.txt index 79a26736b2b..73267ff5ab3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==2.15.4 coverage==7.2.7 freezegun==1.2.2 mock-open==1.4.0 -mypy==1.4.1 +mypy==1.5.0 pre-commit==3.3.3 pydantic==1.10.12 pylint==2.17.4 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index ad4a0f64fe4..779d76078d6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -51,8 +51,9 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { ] ), "disable_error_code": ", ".join(["annotation-unchecked"]), - # Strict_concatenate breaks passthrough ParamSpec typing - "strict_concatenate": "false", + # Impractical in real code + # E.g. this breaks passthrough ParamSpec typing with Concatenate + "extra_checks": "false", } # This is basically the list of checks which is enabled for "strict=true". From a2cf08a1eaaf535d54ae5602ca0b74913545b646 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 13:14:13 +0200 Subject: [PATCH 166/179] Add entity translations to Keymitt ble (#98236) --- homeassistant/components/keymitt_ble/entity.py | 3 ++- homeassistant/components/keymitt_ble/strings.json | 7 +++++++ homeassistant/components/keymitt_ble/switch.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py index b61f8a3c24d..a9294bce239 100644 --- a/homeassistant/components/keymitt_ble/entity.py +++ b/homeassistant/components/keymitt_ble/entity.py @@ -16,11 +16,12 @@ from .coordinator import MicroBotDataUpdateCoordinator class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordinator]): """Generic entity for all MicroBots.""" + _attr_has_entity_name = True + def __init__(self, coordinator, config_entry): """Initialise the entity.""" super().__init__(coordinator) self._address = self.coordinator.ble_device.address - self._attr_name = "Push" self._attr_unique_id = self._address self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_BLUETOOTH, self._address)}, diff --git a/homeassistant/components/keymitt_ble/strings.json b/homeassistant/components/keymitt_ble/strings.json index ab2d4ad9440..2a1f428603e 100644 --- a/homeassistant/components/keymitt_ble/strings.json +++ b/homeassistant/components/keymitt_ble/strings.json @@ -24,6 +24,13 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, + "entity": { + "switch": { + "push": { + "name": "Push" + } + } + }, "services": { "calibrate": { "name": "Calibrate", diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 3e5883ae5d0..4c9f0c335a7 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -43,7 +43,7 @@ async def async_setup_entry( class MicroBotBinarySwitch(MicroBotEntity, SwitchEntity): """MicroBot switch class.""" - _attr_has_entity_name = True + _attr_translation_key = "push" async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" From 97f3199d6d372ca5f5403ae9ff88904587748057 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Aug 2023 13:14:47 +0200 Subject: [PATCH 167/179] Do not add entities with invalid device info (#98150) --- homeassistant/helpers/entity_platform.py | 9 ++++++-- tests/helpers/test_entity_platform.py | 26 ++++++++++++++---------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 9d6a1d0e1d2..c164e3b1052 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -618,8 +618,13 @@ class EntityPlatform: **device_info, ) except dev_reg.DeviceInfoError as exc: - self.logger.error("Ignoring invalid device info: %s", str(exc)) - device = None + self.logger.error( + "%s: Not adding entity with invalid device info: %s", + self.platform_name, + str(exc), + ) + entity.add_to_platform_abort() + return else: device = None diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 77914a49894..0bbfedb8926 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1853,23 +1853,27 @@ async def test_device_name_defaulting_config_entry( @pytest.mark.parametrize( - ("device_info"), + ("device_info", "number_of_entities"), [ # No identifiers - {}, - {"name": "bla"}, - {"default_name": "bla"}, + ({}, 1), # Empty device info does not prevent the entity from being created + ({"name": "bla"}, 0), + ({"default_name": "bla"}, 0), # Match multiple types - { - "identifiers": {("hue", "1234")}, - "name": "bla", - "default_name": "yo", - }, + ( + { + "identifiers": {("hue", "1234")}, + "name": "bla", + "default_name": "yo", + }, + 0, + ), ], ) async def test_device_type_error_checking( hass: HomeAssistant, device_info: dict, + number_of_entities: int, ) -> None: """Test catching invalid device info.""" @@ -1895,6 +1899,6 @@ async def test_device_type_error_checking( dev_reg = dr.async_get(hass) assert len(dev_reg.devices) == 0 - # Entity should still be registered ent_reg = er.async_get(hass) - assert ent_reg.async_get("test_domain.test_qwer") is not None + assert len(ent_reg.entities) == number_of_entities + assert len(hass.states.async_all()) == number_of_entities From fe794e2be3778ecf7204baae8ae9ea2bc92b6736 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 11 Aug 2023 04:47:49 -0700 Subject: [PATCH 168/179] Fix Opower utilities that have different ReadResolution than previously assumed (#97823) --- .../components/opower/coordinator.py | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index b346df1211c..4e2b68df579 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -12,6 +12,7 @@ from opower import ( InvalidAuth, MeterType, Opower, + ReadResolution, ) from homeassistant.components.recorder import get_instance @@ -177,44 +178,55 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): """Get all cost reads since account activation but at different resolutions depending on age. - month resolution for all years (since account activation) - - day resolution for past 3 years - - hour resolution for past 2 months, only for electricity, not gas + - day resolution for past 3 years (if account's read resolution supports it) + - hour resolution for past 2 months (if account's read resolution supports it) """ cost_reads = [] + start = None - end = datetime.now() - timedelta(days=3 * 365) + end = datetime.now() + if account.read_resolution != ReadResolution.BILLING: + end -= timedelta(days=3 * 365) cost_reads += await self.api.async_get_cost_reads( account, AggregateType.BILL, start, end ) + if account.read_resolution == ReadResolution.BILLING: + return cost_reads + start = end if not cost_reads else cost_reads[-1].end_time - end = ( - datetime.now() - timedelta(days=2 * 30) - if account.meter_type == MeterType.ELEC - else datetime.now() - ) + end = datetime.now() + if account.read_resolution != ReadResolution.DAY: + end -= timedelta(days=2 * 30) cost_reads += await self.api.async_get_cost_reads( account, AggregateType.DAY, start, end ) - if account.meter_type == MeterType.ELEC: - start = end if not cost_reads else cost_reads[-1].end_time - end = datetime.now() - cost_reads += await self.api.async_get_cost_reads( - account, AggregateType.HOUR, start, end - ) + if account.read_resolution == ReadResolution.DAY: + return cost_reads + + start = end if not cost_reads else cost_reads[-1].end_time + end = datetime.now() + cost_reads += await self.api.async_get_cost_reads( + account, AggregateType.HOUR, start, end + ) return cost_reads async def _async_get_recent_cost_reads( self, account: Account, last_stat_time: float ) -> list[CostRead]: - """Get cost reads within the past 30 days to allow corrections in data from utilities. - - Hourly for electricity, daily for gas. - """ + """Get cost reads within the past 30 days to allow corrections in data from utilities.""" + if account.read_resolution in [ + ReadResolution.HOUR, + ReadResolution.HALF_HOUR, + ReadResolution.QUARTER_HOUR, + ]: + aggregate_type = AggregateType.HOUR + elif account.read_resolution == ReadResolution.DAY: + aggregate_type = AggregateType.DAY + else: + aggregate_type = AggregateType.BILL return await self.api.async_get_cost_reads( account, - AggregateType.HOUR - if account.meter_type == MeterType.ELEC - else AggregateType.DAY, + aggregate_type, datetime.fromtimestamp(last_stat_time) - timedelta(days=30), datetime.now(), ) From 27876b929b85069bca60d4f8cf1dc1cf423200a5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 13:53:11 +0200 Subject: [PATCH 169/179] Migrate iZone to has entity name (#98234) --- homeassistant/components/izone/climate.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index cac29641e28..2dcdd72f6b9 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -133,6 +133,8 @@ class ControllerDevice(ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_should_poll = False _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True + _attr_name = None def __init__(self, controller: Controller) -> None: """Initialise ControllerDevice.""" @@ -169,7 +171,7 @@ class ControllerDevice(ClimateEntity): identifiers={(IZONE, self.unique_id)}, manufacturer="IZone", model=self._controller.sys_type, - name=self.name, + name=f"iZone Controller {self._controller.device_uid}", ) # Create the zones @@ -256,11 +258,6 @@ class ControllerDevice(ClimateEntity): """Return the ID of the controller device.""" return self._controller.device_uid - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"iZone Controller {self._controller.device_uid}" - @property def extra_state_attributes(self) -> Mapping[str, Any]: """Return the optional state attributes.""" @@ -444,13 +441,14 @@ class ZoneDevice(ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__(self, controller: ControllerDevice, zone: Zone) -> None: """Initialise ZoneDevice.""" self._controller = controller self._zone = zone - self._name = zone.name.title() if zone.type != Zone.Type.AUTO: self._state_to_pizone = { @@ -471,7 +469,7 @@ class ZoneDevice(ClimateEntity): }, manufacturer="IZone", model=zone.type.name.title(), - name=self.name, + name=zone.name.title(), via_device=(IZONE, controller.unique_id), ) @@ -500,7 +498,6 @@ class ZoneDevice(ClimateEntity): return if not self.available: return - self._name = zone.name.title() self.async_write_ha_state() self.async_on_remove( @@ -517,11 +514,6 @@ class ZoneDevice(ClimateEntity): """Return the ID of the controller device.""" return f"{self._controller.unique_id}_z{self._zone.index + 1}" - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @property @_return_on_connection_error(0) def supported_features(self) -> ClimateEntityFeature: From 3499ba3a9e2b5baca1bb82727814ff5c4862e358 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 13:54:57 +0200 Subject: [PATCH 170/179] Add device classes to Buienradar (#98151) --- homeassistant/components/buienradar/sensor.py | 8 ++------ .../components/buienradar/strings.json | 18 ------------------ 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index b5c6e9cf32c..e52000edf7f 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -129,14 +129,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="humidity", - translation_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature", - translation_key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -150,7 +149,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="windspeed", - translation_key="windspeed", native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, @@ -174,7 +172,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="pressure", - translation_key="pressure", + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.HPA, icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, @@ -194,14 +192,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="precipitation", - translation_key="precipitation", native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, ), SensorEntityDescription( key="irradiance", - translation_key="irradiance", device_class=SensorDeviceClass.IRRADIANCE, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/buienradar/strings.json b/homeassistant/components/buienradar/strings.json index f254f7602f8..2141f420167 100644 --- a/homeassistant/components/buienradar/strings.json +++ b/homeassistant/components/buienradar/strings.json @@ -84,18 +84,9 @@ "feeltemperature": { "name": "Feel temperature" }, - "humidity": { - "name": "[%key:component::sensor::entity_component::humidity::name%]" - }, - "temperature": { - "name": "[%key:component::sensor::entity_component::temperature::name%]" - }, "groundtemperature": { "name": "Ground temperature" }, - "windspeed": { - "name": "[%key:component::sensor::entity_component::wind_speed::name%]" - }, "windforce": { "name": "Wind force" }, @@ -105,21 +96,12 @@ "windazimuth": { "name": "Wind direction azimuth" }, - "pressure": { - "name": "[%key:component::sensor::entity_component::pressure::name%]" - }, "visibility": { "name": "[%key:component::weather::entity_component::_::state_attributes::visibility::name%]" }, "windgust": { "name": "Wind gust" }, - "precipitation": { - "name": "[%key:component::sensor::entity_component::precipitation::name%]" - }, - "irradiance": { - "name": "[%key:component::sensor::entity_component::irradiance::name%]" - }, "precipitation_forecast_average": { "name": "Precipitation forecast average" }, From e6ba6c295c5116ebb091b901de2fe545b807aef2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 11 Aug 2023 13:55:38 +0200 Subject: [PATCH 171/179] Add base entity to Garages Amsterdam (#98172) --- .coveragerc | 1 + .../garages_amsterdam/binary_sensor.py | 17 ++++--------- .../components/garages_amsterdam/entity.py | 24 +++++++++++++++++++ .../components/garages_amsterdam/sensor.py | 19 +++++---------- 4 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/garages_amsterdam/entity.py diff --git a/.coveragerc b/.coveragerc index 6aa0c8cce06..347e5527ee4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -421,6 +421,7 @@ omit = homeassistant/components/garadget/cover.py homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/binary_sensor.py + homeassistant/components/garages_amsterdam/entity.py homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/gc100/* homeassistant/components/geniushub/* diff --git a/homeassistant/components/garages_amsterdam/binary_sensor.py b/homeassistant/components/garages_amsterdam/binary_sensor.py index 5b444146624..41237fc7423 100644 --- a/homeassistant/components/garages_amsterdam/binary_sensor.py +++ b/homeassistant/components/garages_amsterdam/binary_sensor.py @@ -8,13 +8,10 @@ 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 homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import get_coordinator -from .const import ATTRIBUTION +from .entity import GaragesAmsterdamEntity BINARY_SENSORS = { "state", @@ -30,27 +27,23 @@ async def async_setup_entry( coordinator = await get_coordinator(hass) async_add_entities( - GaragesamsterdamBinarySensor( + GaragesAmsterdamBinarySensor( coordinator, config_entry.data["garage_name"], info_type ) for info_type in BINARY_SENSORS ) -class GaragesamsterdamBinarySensor(CoordinatorEntity, BinarySensorEntity): +class GaragesAmsterdamBinarySensor(GaragesAmsterdamEntity, BinarySensorEntity): """Binary Sensor representing garages amsterdam data.""" - _attr_attribution = ATTRIBUTION _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__( self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str ) -> None: """Initialize garages amsterdam binary sensor.""" - super().__init__(coordinator) - self._attr_unique_id = f"{garage_name}-{info_type}" - self._garage_name = garage_name - self._info_type = info_type + super().__init__(coordinator, garage_name, info_type) self._attr_name = garage_name @property diff --git a/homeassistant/components/garages_amsterdam/entity.py b/homeassistant/components/garages_amsterdam/entity.py new file mode 100644 index 00000000000..894506f7da9 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/entity.py @@ -0,0 +1,24 @@ +"""Generic entity for Garages Amsterdam.""" +from __future__ import annotations + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ATTRIBUTION + + +class GaragesAmsterdamEntity(CoordinatorEntity): + """Base Entity for garages amsterdam data.""" + + _attr_attribution = ATTRIBUTION + + def __init__( + self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str + ) -> None: + """Initialize garages amsterdam entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{garage_name}-{info_type}" + self._garage_name = garage_name + self._info_type = info_type diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index 252f010dfdb..b4acb36691e 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -5,13 +5,10 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import get_coordinator -from .const import ATTRIBUTION +from .entity import GaragesAmsterdamEntity SENSORS = { "free_space_short": "mdi:car", @@ -29,12 +26,12 @@ async def async_setup_entry( """Defer sensor setup to the shared sensor module.""" coordinator = await get_coordinator(hass) - entities: list[GaragesamsterdamSensor] = [] + entities: list[GaragesAmsterdamSensor] = [] for info_type in SENSORS: if getattr(coordinator.data[config_entry.data["garage_name"]], info_type) != "": entities.append( - GaragesamsterdamSensor( + GaragesAmsterdamSensor( coordinator, config_entry.data["garage_name"], info_type ) ) @@ -42,20 +39,16 @@ async def async_setup_entry( async_add_entities(entities) -class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): +class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity): """Sensor representing garages amsterdam data.""" - _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = "cars" def __init__( self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str ) -> None: """Initialize garages amsterdam sensor.""" - super().__init__(coordinator) - self._attr_unique_id = f"{garage_name}-{info_type}" - self._garage_name = garage_name - self._info_type = info_type + super().__init__(coordinator, garage_name, info_type) self._attr_name = f"{garage_name} - {info_type}".replace("_", " ") self._attr_icon = SENSORS[info_type] From 7f616b0d44e18416b0802cb30322eead52d7840c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 11 Aug 2023 14:11:06 +0200 Subject: [PATCH 172/179] Improve UniFi control PoE mode (#98119) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 4cc45ddb6b8..3b1fa68638b 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==51"], + "requirements": ["aiounifi==52"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index e7496d77298..02503e1f7ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -360,7 +360,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==51 +aiounifi==52 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83ad6578cee..2231346266f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==51 +aiounifi==52 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 8bef39a2fb70da62db4201ee5ee830daf76b6804 Mon Sep 17 00:00:00 2001 From: Hessel Date: Fri, 11 Aug 2023 16:39:29 +0200 Subject: [PATCH 173/179] Wallbox Integration Change Switch Availability (#98111) * change switch availability * remove new test * Update homeassistant/components/wallbox/switch.py Also check super availability. Co-authored-by: G Johansson * black formatting --------- Co-authored-by: G Johansson --- homeassistant/components/wallbox/switch.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wallbox/switch.py b/homeassistant/components/wallbox/switch.py index 3d046d5d241..7a0736f59e7 100644 --- a/homeassistant/components/wallbox/switch.py +++ b/homeassistant/components/wallbox/switch.py @@ -54,11 +54,16 @@ class WallboxSwitch(WallboxEntity, SwitchEntity): @property def available(self) -> bool: """Return the availability of the switch.""" - return self.coordinator.data[CHARGER_STATUS_DESCRIPTION_KEY] in { - ChargerStatus.CHARGING, - ChargerStatus.DISCHARGING, - ChargerStatus.PAUSED, - ChargerStatus.SCHEDULED, + return super().available and self.coordinator.data[ + CHARGER_STATUS_DESCRIPTION_KEY + ] not in { + ChargerStatus.UNKNOWN, + ChargerStatus.UPDATING, + ChargerStatus.ERROR, + ChargerStatus.LOCKED, + ChargerStatus.LOCKED_CAR_CONNECTED, + ChargerStatus.DISCONNECTED, + ChargerStatus.READY, } @property From 2ed11d2900c823972d92aa7397934719ce5979b8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:09:38 +0200 Subject: [PATCH 174/179] Add types-xmltodict dependency (#98268) --- requirements_test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_test.txt b/requirements_test.txt index 73267ff5ab3..41d81eea321 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -47,3 +47,4 @@ types-python-slugify==0.1.2 types-pytz==2023.3.0.0 types-PyYAML==6.0.12.2 types-requests==2.31.0.1 +types-xmltodict==0.13.0.3 From 8fbcffcf9fc772647099a5bd3e13a300a7c49f6b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:09:58 +0200 Subject: [PATCH 175/179] Add types-psutil dependency (#98267) --- homeassistant/components/systemmonitor/sensor.py | 4 ++-- requirements_test.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 7f0866ce62e..4cfbdba4066 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -9,7 +9,7 @@ import logging import os import socket import sys -from typing import Any, cast +from typing import Any import psutil import voluptuous as vol @@ -613,6 +613,6 @@ def _read_cpu_temperature() -> float | None: # check both name and label because some systems embed cpu# in the # name, which makes label not match because label adds cpu# at end. if _label in CPU_SENSOR_PREFIXES or name in CPU_SENSOR_PREFIXES: - return cast(float, round(entry.current, 1)) + return round(entry.current, 1) return None diff --git a/requirements_test.txt b/requirements_test.txt index 41d81eea321..76a94c758b9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -42,6 +42,7 @@ types-enum34==1.1.8 types-ipaddress==1.0.8 types-paho-mqtt==1.6.0.6 types-pkg-resources==0.1.3 +types-psutil==5.9.5 types-python-dateutil==2.8.19.13 types-python-slugify==0.1.2 types-pytz==2023.3.0.0 From 4342a95be0bde6bcc7ac9471c9b01f4b41b8f96d Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 11 Aug 2023 13:31:47 -0400 Subject: [PATCH 176/179] Add Enphase switch platform and grid enable switch (#98261) * Add Enphase switch platform and grid enable switch * Update dependency * Fix docstrings * Update .coveragerc --- .coveragerc | 1 + .../components/enphase_envoy/const.py | 2 +- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/strings.json | 5 + .../components/enphase_envoy/switch.py | 113 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/enphase_envoy/switch.py diff --git a/.coveragerc b/.coveragerc index 347e5527ee4..d36153ccca7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -306,6 +306,7 @@ omit = homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/entity.py homeassistant/components/enphase_envoy/sensor.py + homeassistant/components/enphase_envoy/switch.py homeassistant/components/entur_public_transport/* homeassistant/components/environment_canada/__init__.py homeassistant/components/environment_canada/camera.py diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index d10cc0b9511..828abe8fe4c 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -5,6 +5,6 @@ from homeassistant.const import Platform DOMAIN = "enphase_envoy" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 7cd107a3e67..f500ac538e7 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.3.0"], + "requirements": ["pyenphase==1.4.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 915fee94e2a..f42e44d7afa 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -64,6 +64,11 @@ "lifetime_consumption": { "name": "Lifetime energy consumption" } + }, + "switch": { + "grid_enabled": { + "name": "Grid enabled" + } } } } diff --git a/homeassistant/components/enphase_envoy/switch.py b/homeassistant/components/enphase_envoy/switch.py new file mode 100644 index 00000000000..820b904e070 --- /dev/null +++ b/homeassistant/components/enphase_envoy/switch.py @@ -0,0 +1,113 @@ +"""Switch platform for Enphase Envoy solar energy monitor.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import logging +from typing import Any + +from pyenphase import Envoy, EnvoyEnpower + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import EnphaseUpdateCoordinator +from .entity import EnvoyBaseEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class EnvoyEnpowerRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnpower], bool] + turn_on_fn: Callable[[Envoy], Coroutine[Any, Any, dict[str, Any]]] + turn_off_fn: Callable[[Envoy], Coroutine[Any, Any, dict[str, Any]]] + + +@dataclass +class EnvoyEnpowerSwitchEntityDescription( + SwitchEntityDescription, EnvoyEnpowerRequiredKeysMixin +): + """Describes an Envoy Enpower switch entity.""" + + +ENPOWER_GRID_SWITCH = EnvoyEnpowerSwitchEntityDescription( + key="mains_admin_state", + translation_key="grid_enabled", + value_fn=lambda enpower: enpower.mains_admin_state == "closed", + turn_on_fn=lambda envoy: envoy.go_on_grid(), + turn_off_fn=lambda envoy: envoy.go_off_grid(), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Enphase Envoy switch platform.""" + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + envoy_data = coordinator.envoy.data + assert envoy_data is not None + envoy_serial_num = config_entry.unique_id + assert envoy_serial_num is not None + entities: list[SwitchEntity] = [] + if envoy_data.enpower: + entities.extend( + [ + EnvoyEnpowerSwitchEntity( + coordinator, ENPOWER_GRID_SWITCH, envoy_data.enpower + ) + ] + ) + async_add_entities(entities) + + +class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity): + """Representation of an Enphase Enpower switch entity.""" + + entity_description: EnvoyEnpowerSwitchEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyEnpowerSwitchEntityDescription, + enpower: EnvoyEnpower, + ) -> None: + """Initialize the Enphase Enpower switch entity.""" + super().__init__(coordinator, description) + self.envoy = coordinator.envoy + self.enpower = enpower + self._serial_number = enpower.serial_number + self._attr_unique_id = f"{self._serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + manufacturer="Enphase", + model="Enpower", + name=f"Enpower {self._serial_number}", + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + ) + + @property + def is_on(self) -> bool: + """Return the state of the Enpower switch.""" + enpower = self.data.enpower + assert enpower is not None + return self.entity_description.value_fn(enpower) + + async def async_turn_on(self): + """Turn on the Enpower switch.""" + await self.entity_description.turn_on_fn(self.envoy) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self): + """Turn off the Enpower switch.""" + await self.entity_description.turn_off_fn(self.envoy) + await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 02503e1f7ec..4f99f44c9f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1665,7 +1665,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.3.0 +pyenphase==1.4.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2231346266f..cc1bc38b233 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.3.0 +pyenphase==1.4.0 # homeassistant.components.everlights pyeverlights==0.1.0 From ff0566b11f1dd9599972eb2ebd5e007ea78bfb07 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 Aug 2023 23:07:06 +0200 Subject: [PATCH 177/179] Fix deque import (#98269) --- homeassistant/components/stream/recorder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 258457a3d82..a334171abb8 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,6 +1,7 @@ """Provide functionality to record stream.""" from __future__ import annotations +from collections import deque from io import DEFAULT_BUFFER_SIZE, BytesIO import logging import os @@ -19,8 +20,6 @@ from .core import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings from .fmp4utils import read_init, transform_init if TYPE_CHECKING: - import deque - from homeassistant.components.camera import DynamicStreamSettings _LOGGER = logging.getLogger(__name__) From 78ac36e3fe890ff7729ec3ff467b04ecd49ce17e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 Aug 2023 23:08:21 +0200 Subject: [PATCH 178/179] Improve met_eireann generic typing (#98278) --- homeassistant/components/met_eireann/__init__.py | 5 +++-- homeassistant/components/met_eireann/weather.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index a5b096b5554..042eb6f458f 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -1,6 +1,7 @@ """The met_eireann component.""" from datetime import timedelta import logging +from typing import Self import meteireann @@ -33,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b weather_data = MetEireannWeatherData(hass, config_entry.data, raw_weather_data) - async def _async_update_data(): + async def _async_update_data() -> MetEireannWeatherData: """Fetch data from Met Éireann.""" try: return await weather_data.fetch_data() @@ -78,7 +79,7 @@ class MetEireannWeatherData: self.daily_forecast = None self.hourly_forecast = None - async def fetch_data(self): + async def fetch_data(self) -> Self: """Fetch data from API - (current weather and forecast).""" await self._weather_data.fetching_data() self.current_weather_data = self._weather_data.get_current_weather() diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 1356dbe0c24..e31951ea8a2 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -22,9 +22,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import dt as dt_util +from . import MetEireannWeatherData from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP _LOGGER = logging.getLogger(__name__) @@ -54,7 +58,9 @@ async def async_setup_entry( ) -class MetEireannWeather(CoordinatorEntity, WeatherEntity): +class MetEireannWeather( + CoordinatorEntity[DataUpdateCoordinator[MetEireannWeatherData]], WeatherEntity +): """Implementation of a Met Éireann weather condition.""" _attr_attribution = "Data provided by Met Éireann" From 58194a5eb409cff5d37c9f7c1409bdad80ce84a5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 Aug 2023 23:08:52 +0200 Subject: [PATCH 179/179] Improve wake_word generic typing (#98279) --- homeassistant/components/wake_word/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index f33d06c64da..895dababd54 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -43,7 +43,7 @@ def async_get_wake_word_detection_entity( hass: HomeAssistant, entity_id: str ) -> WakeWordDetectionEntity | None: """Return wake word entity.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[WakeWordDetectionEntity] = hass.data[DOMAIN] return component.get_entity(entity_id)